feat: Material 3 Expressive анимации загрузки и нажатий

This commit is contained in:
klockky
2026-07-14 18:38:25 +03:00
parent e2949288aa
commit 3b60851037
49 changed files with 546 additions and 572 deletions
+2 -8
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/utils/format.dart';
import '../widgets/small_spinner.dart';
class DebugCacheSection extends StatelessWidget {
final int cacheSize;
@@ -129,14 +130,7 @@ class DebugCacheSection extends StatelessWidget {
),
),
if (clearingCache)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onSurfaceVariant,
),
),
SmallSpinner(size: 20, color: cs.onSurfaceVariant),
],
),
),
+2 -5
View File
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../widgets/custom_notification.dart';
import '../widgets/glossy_pill.dart';
import '../widgets/small_spinner.dart';
class DebugIdSearchSection extends StatelessWidget {
final TextEditingController idController;
@@ -72,11 +73,7 @@ class DebugIdSearchSection extends StatelessWidget {
FilledButton(
onPressed: isSearching ? null : onSearch,
child: isSearching
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 20)
: const Icon(Symbols.search, size: 20),
),
],
+2 -5
View File
@@ -6,6 +6,7 @@ import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../main.dart';
import '../widgets/glossy_pill.dart';
import '../widgets/small_spinner.dart';
class DebugSyncProbeSection extends StatefulWidget {
const DebugSyncProbeSection({super.key});
@@ -147,11 +148,7 @@ class _DebugSyncProbeSectionState extends State<DebugSyncProbeSection> {
),
),
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 20)
: const Text('Отправить'),
),
if (_result != null) ...[
@@ -11,6 +11,7 @@ import '../../../core/utils/sms_code_listener.dart';
import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/login_success_screen.dart';
import '../../widgets/small_spinner.dart';
class CodeConfirmationScreen extends StatefulWidget {
final String phoneNumber;
@@ -525,14 +526,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
borderRadius: BorderRadius.circular(50),
),
child: recovering
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimaryContainer,
),
)
? SmallSpinner(size: 24, color: cs.onPrimaryContainer)
: Icon(
Icons.arrow_forward,
color: _codeController.text.length == 6
+4 -7
View File
@@ -18,6 +18,7 @@ import '../digital_id/digital_id_web_screen.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/adaptive_shell.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../../backend/api.dart';
import '../../../core/protocol/packet.dart';
import '../../../main.dart';
@@ -998,13 +999,9 @@ class _LoginScreenState extends State<LoginScreen> {
borderRadius: BorderRadius.circular(50),
),
child: _isPhoneValid && !_isOnline
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimaryContainer,
),
? SmallSpinner(
size: 24,
color: cs.onPrimaryContainer,
)
: Icon(
Icons.arrow_forward,
@@ -3,6 +3,7 @@ import '../../../core/protocol/packet.dart';
import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/login_success_screen.dart';
import '../../widgets/small_spinner.dart';
import 'session_stale_recovery.dart';
class Password2FAScreen extends StatefulWidget {
@@ -199,14 +200,7 @@ class _Password2FAScreenState extends State<Password2FAScreen>
borderRadius: BorderRadius.circular(50),
),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimaryContainer,
),
)
? SmallSpinner(size: 24, color: cs.onPrimaryContainer)
: Icon(
Icons.arrow_forward,
color: _passwordController.text.isNotEmpty
@@ -6,6 +6,7 @@ import '../../../backend/modules/account.dart';
import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/login_success_screen.dart';
import '../../widgets/small_spinner.dart';
class RegistrationScreen extends StatefulWidget {
final String phoneNumber;
@@ -110,14 +111,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
child: _isSubmitting
? SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: cs.onSurfaceVariant,
),
)
? SmallSpinner(size: 22, color: cs.onSurfaceVariant)
: Icon(
Icons.arrow_forward,
color: _canSubmit ? cs.onPrimaryContainer : cs.onSurfaceVariant,
@@ -8,6 +8,7 @@ import '../../../models/spoof_profile.dart';
import '../../widgets/adaptive_shell.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/section_header.dart';
import '../../widgets/small_spinner.dart';
class TokenLoginScreen extends StatefulWidget {
final int? returnToAccountId;
@@ -143,11 +144,7 @@ class _TokenLoginScreenState extends State<TokenLoginScreen> {
shape: const StadiumBorder(),
),
child: _isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 22)
: Text(l10n.tokenLoginButton),
),
),
+2 -8
View File
@@ -24,6 +24,7 @@ import '../../../l10n/app_localizations.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import 'komet_hub.dart';
const Color _kEndRed = Color(0xFFE5484D);
@@ -1261,14 +1262,7 @@ class _CallButton extends StatelessWidget {
depth: 9,
child: Center(
child: busy
? SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation(foreground),
),
)
? SmallSpinner(size: 22, color: foreground)
: Icon(icon, color: foreground, size: 26, fill: 1),
),
),
+2 -1
View File
@@ -12,6 +12,7 @@ import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/chat_menu_overlay.dart';
import '../../widgets/small_spinner.dart';
import 'call_screen.dart';
class CallsTab extends StatefulWidget {
@@ -421,7 +422,7 @@ class _CallsTabState extends State<CallsTab> {
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: filteredCalls.isEmpty
? Center(
child: Text(
@@ -9,6 +9,7 @@ import 'package:komet/core/utils/format.dart';
import 'package:komet/frontend/widgets/animated_lottie_icon.dart';
import 'package:komet/frontend/widgets/glossy_pill.dart';
import 'package:komet/frontend/widgets/komet_avatar.dart';
import 'package:komet/frontend/widgets/small_spinner.dart';
import 'package:komet/frontend/screens/chats/chat/chat_search_controller.dart';
import 'package:komet/frontend/screens/chats/chat/message_search_result.dart';
@@ -168,14 +169,7 @@ class SearchOverlay extends StatelessWidget {
}
if (loading) {
return Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: cs.onSurfaceVariant,
),
),
child: SmallSpinner(size: 26, color: cs.onSurfaceVariant),
);
}
return ValueListenableBuilder<bool>(
+204 -196
View File
@@ -16,6 +16,7 @@ import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/swipe_route.dart';
import '../../widgets/sliding_pill_nav.dart';
import '../../widgets/springy_tap.dart';
import '../../widgets/formatted_message_text.dart';
import '../../../core/utils/format.dart';
import '../../../core/utils/text_format.dart';
@@ -2550,219 +2551,226 @@ class _ChatListScreenState extends State<ChatListScreen>
)
: null,
);
return InkWell(
return SpringyTap(
key: ValueKey('chat_$id'),
onTap: () {
if (widget.forwardMode) {
Navigator.of(context).pop(
ForwardTarget(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
);
return;
}
if (_isSelectionMode) {
_toggleSelection(id);
return;
}
if (imageUrl.isNotEmpty) {
unawaited(
precacheImage(
CachedNetworkImageProvider(
imageUrl,
maxWidth: kAvatarThumbSize,
maxHeight: kAvatarThumbSize,
child: InkWell(
onTap: () {
if (widget.forwardMode) {
Navigator.of(context).pop(
ForwardTarget(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
);
return;
}
if (_isSelectionMode) {
_toggleSelection(id);
return;
}
if (imageUrl.isNotEmpty) {
unawaited(
precacheImage(
CachedNetworkImageProvider(
imageUrl,
maxWidth: kAvatarThumbSize,
maxHeight: kAvatarThumbSize,
),
context,
),
);
}
if (widget.onChatSelected != null) {
widget.onChatSelected!(
DesktopChatSelection(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
);
} else {
pushSwipeable(
context,
),
);
}
if (widget.onChatSelected != null) {
widget.onChatSelected!(
DesktopChatSelection(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
);
} else {
pushSwipeable(
context,
(context) => ChatScreen(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
);
}
},
onLongPress: widget.forwardMode ? null : () => _toggleSelection(id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
color: isSelected
? cs.primary.withValues(alpha: 0.08)
: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
children: [
avatarCircle,
if (isSelected)
Positioned(
right: -2,
bottom: -2,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: cs.primary,
shape: BoxShape.circle,
border: Border.all(color: cs.surface, width: 2),
),
child: Icon(
Symbols.check,
color: cs.onPrimary,
size: 14,
),
),
)
else if (presenceUserId != 0)
Positioned(
right: 0,
bottom: 0,
child: OnlineDot(
userId: presenceUserId,
borderColor: cs.surface,
),
),
],
(context) => ChatScreen(
chatId: int.parse(id),
name: name,
imageUrl: imageUrl,
chatType: chatType,
),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 48,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.1,
);
}
},
onLongPress: widget.forwardMode ? null : () => _toggleSelection(id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
color: isSelected
? cs.primary.withValues(alpha: 0.08)
: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
children: [
avatarCircle,
if (isSelected)
Positioned(
right: -2,
bottom: -2,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: cs.primary,
shape: BoxShape.circle,
border: Border.all(color: cs.surface, width: 2),
),
child: Icon(
Symbols.check,
color: cs.onPrimary,
size: 14,
),
),
)
else if (presenceUserId != 0)
Positioned(
right: 0,
bottom: 0,
child: OnlineDot(
userId: presenceUserId,
borderColor: cs.surface,
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 48,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
name,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
if (isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
],
],
],
),
),
),
if (isMuted) ...[
const SizedBox(width: 4),
Icon(
Symbols.notifications_off,
color: cs.outlineVariant,
size: 14,
weight: 400,
if (isMuted) ...[
const SizedBox(width: 4),
Icon(
Symbols.notifications_off,
color: cs.outlineVariant,
size: 14,
weight: 400,
),
],
if (isPinned) ...[
const SizedBox(width: 4),
Icon(
Symbols.keep,
color: cs.outlineVariant,
size: 14,
weight: 400,
),
],
const SizedBox(width: 8),
Text(
time,
style: TextStyle(
color: cs.outline,
fontSize: 12,
),
),
],
if (isPinned) ...[
const SizedBox(width: 4),
Icon(
Symbols.keep,
color: cs.outlineVariant,
size: 14,
weight: 400,
),
],
const SizedBox(width: 8),
Text(
time,
style: TextStyle(color: cs.outline, fontSize: 12),
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ActivitySubtitle(
chatId: int.tryParse(id) ?? 0,
child: messageLine,
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ActivitySubtitle(
chatId: int.tryParse(id) ?? 0,
child: messageLine,
),
),
),
?statusIcon,
const SizedBox(width: 8),
if (unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: isMuted
? cs.surfaceContainerHighest
: cs.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(
unreadCount.toString(),
style: TextStyle(
color: isMuted ? cs.outline : cs.onPrimary,
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.1,
?statusIcon,
const SizedBox(width: 8),
if (unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: isMuted
? cs.surfaceContainerHighest
: cs.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(
unreadCount.toString(),
style: TextStyle(
color: isMuted
? cs.outline
: cs.onPrimary,
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.1,
),
),
)
else if (isRead)
Icon(
Symbols.done_all,
color: cs.primary,
size: 16,
weight: 400,
),
)
else if (isRead)
Icon(
Symbols.done_all,
color: cs.primary,
size: 16,
weight: 400,
),
],
],
),
),
),
],
],
),
),
),
),
],
],
),
),
),
),
+2 -8
View File
@@ -87,6 +87,7 @@ import '../../widgets/lottie_image.dart';
import '../../widgets/attachment_panel.dart';
import '../../widgets/attachment/attachment_sheet.dart';
import '../../widgets/sticker_pack_sheet.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/swipe_to_pop.dart';
import '../../widgets/swipe_route.dart';
import '../../widgets/schedule_time_picker.dart';
@@ -4610,14 +4611,7 @@ class _ChatScreenState extends State<ChatScreen>
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: cs.onSurfaceVariant,
),
),
child: SmallSpinner(size: 22, color: cs.onSurfaceVariant),
),
);
}
@@ -13,6 +13,7 @@ import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/swipe_route.dart';
import 'chat_screen.dart';
@@ -295,7 +296,7 @@ class _CreateGroupFlowState extends State<_CreateGroupFlow> {
child: _loading
? const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator()),
child: Center(child: SmallSpinner(size: 36)),
)
: ListView.builder(
padding: EdgeInsets.zero,
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart'
show ExpressiveRefreshIndicator;
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/messages.dart';
@@ -15,6 +17,7 @@ import '../../widgets/confirm_dialog.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/schedule_time_picker.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
class ScheduledMessagesScreen extends StatefulWidget {
final int chatId;
@@ -258,10 +261,10 @@ class _ScheduledMessagesScreenState extends State<ScheduledMessagesScreen> {
),
),
body: _loading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: _messages.isEmpty
? _empty(cs)
: RefreshIndicator(
: ExpressiveRefreshIndicator(
onRefresh: _load,
child: ListView.separated(
padding: const EdgeInsets.all(16),
@@ -10,6 +10,7 @@ import '../../../core/storage/app_database.dart';
import '../../../core/utils/debouncer.dart';
import '../../../core/utils/names.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/swipe_route.dart';
import '../contacts/open_contact_profile.dart';
import 'chat_screen.dart';
@@ -232,7 +233,7 @@ class _SearchScreenState extends State<SearchScreen> {
}
if (!hasResults) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: SmallSpinner(size: 36));
}
return _buildHint(cs, Symbols.search_off, 'Ничего не найдено');
}
+76 -76
View File
@@ -11,6 +11,8 @@ import '../../../models/contact_info.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/springy_tap.dart';
import '../chats/chat_info_screen.dart';
import 'nfc_exchange_sheet.dart';
import 'open_contact_profile.dart';
@@ -115,83 +117,85 @@ class _ContactsTabState extends State<ContactsTab> {
.trim();
final nameToDisplay = fullName.isEmpty ? '+${contact.phone}' : fullName;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => openContactDialogProfile(
context,
contactId: contact.id,
name: nameToDisplay,
avatarUrl: contact.baseUrl,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: cs.primary.withValues(alpha: 0.1),
width: 1,
return SpringyTap(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => openContactDialogProfile(
context,
contactId: contact.id,
name: nameToDisplay,
avatarUrl: contact.baseUrl,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: cs.primary.withValues(alpha: 0.1),
width: 1,
),
),
child: KometAvatar(
name: nameToDisplay,
imageUrl: contact.baseUrl,
size: 48,
),
),
child: KometAvatar(
name: nameToDisplay,
imageUrl: contact.baseUrl,
size: 48,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
nameToDisplay,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
nameToDisplay,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (contact.isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
if (contact.isVerified) ...[
const SizedBox(width: 4),
Icon(
Symbols.verified,
color: cs.primary,
size: 16,
weight: 600,
fill: 1,
),
],
],
],
),
const SizedBox(height: 4),
Text(
contact.updateTime > 0
? 'Был(а) недавно'
: '+${contact.phone}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 4),
Text(
contact.updateTime > 0
? 'Был(а) недавно'
: '+${contact.phone}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
],
),
),
),
),
@@ -243,7 +247,7 @@ class _ContactsTabState extends State<ContactsTab> {
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: _contacts.isEmpty
? Center(
child: Text(
@@ -457,11 +461,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 20)
: const Text('Найти'),
),
],
@@ -15,6 +15,7 @@ import '../../../main.dart';
import '../../../models/contact_info.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/small_spinner.dart';
enum _Stage {
checking,
@@ -243,7 +244,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
case _Stage.checking:
return const Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: CircularProgressIndicator(),
child: SmallSpinner(size: 36),
);
case _Stage.unsupported:
return _message(cs, Symbols.nfc, l10n.nfcUnsupported);
@@ -420,11 +421,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: _stage == _Stage.adding
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 20)
: Text(
_stage == _Stage.added
? l10n.nfcAdded
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart'
show ExpressiveRefreshIndicator;
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/digital_id.dart';
@@ -10,6 +12,7 @@ import '../../../models/digital_id.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/error_view.dart';
import '../../widgets/small_spinner.dart';
import '../webapp/web_app_screen.dart';
String _documentLabel(AppLocalizations l10n, String type) {
@@ -198,7 +201,7 @@ class _DigitalIdScreenState extends State<DigitalIdScreen> {
Widget _buildBody(ColorScheme cs) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: SmallSpinner(size: 36));
}
if (_error != null) {
return ErrorView(message: _error!, onRetry: _load);
@@ -206,7 +209,7 @@ class _DigitalIdScreenState extends State<DigitalIdScreen> {
if (_docs == null) {
return _buildOnboarding(cs);
}
return RefreshIndicator(
return ExpressiveRefreshIndicator(
onRefresh: _load,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -278,11 +281,7 @@ class _DigitalIdScreenState extends State<DigitalIdScreen> {
child: FilledButton.icon(
onPressed: _busy ? null : _linkGosuslugi,
icon: _busy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 18)
: const Icon(Symbols.link, size: 18),
label: Text(
l10n.digitalIdLinkGosuslugiButton,
@@ -18,6 +18,7 @@ import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
enum _EnvState { loading, notConfigured, ready }
@@ -333,7 +334,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
),
),
body: switch (_envState) {
_EnvState.loading => const Center(child: CircularProgressIndicator()),
_EnvState.loading => const Center(child: SmallSpinner(size: 36)),
_EnvState.notConfigured => _buildNotConfigured(cs),
_EnvState.ready => _buildReady(cs),
},
@@ -375,14 +376,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
),
),
child: _isCreatingEnv
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
? SmallSpinner(size: 18, color: cs.onPrimary)
: Text(
l10n.cloudStorageStart,
style: const TextStyle(
@@ -1053,14 +1047,7 @@ class _FileDetailsSheetState extends State<_FileDetailsSheet> {
),
const SizedBox(width: 8),
_loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.primary,
),
)
? SmallSpinner(size: 20, color: cs.primary)
: IconButton(
icon: Icon(
isExpired ? Symbols.add_link : Symbols.content_copy,
@@ -1221,14 +1208,7 @@ class _SendByIdSheetState extends State<_SendByIdSheet> {
),
),
child: _sending
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
? SmallSpinner(size: 18, color: cs.onPrimary)
: Text(
l10n.cloudStorageSend,
style: const TextStyle(fontWeight: FontWeight.w600),
@@ -14,6 +14,7 @@ import '../../widgets/custom_notification.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/prompt_dialog.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/web_qr_login.dart';
import 'web_qr_scan_screen.dart';
@@ -533,14 +534,10 @@ class _DevicesScreenState extends State<DevicesScreen>
child: Container(
padding: const EdgeInsets.all(4),
child: isLoading
? SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onSurfaceVariant.withValues(
alpha: 0.5,
),
? SmallSpinner(
size: 14,
color: cs.onSurfaceVariant.withValues(
alpha: 0.5,
),
)
: Icon(
@@ -8,6 +8,7 @@ import '../../../main.dart' show accountModule, fileUploader, KometApp;
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/small_spinner.dart';
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({super.key});
@@ -180,11 +181,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
TextButton(
onPressed: _isLoading || _isSaving ? null : _saveName,
child: _isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SmallSpinner(size: 16)
: Text(
l10n?.editProfileSave ?? 'Save',
style: TextStyle(
@@ -196,7 +193,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -9,6 +9,7 @@ import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/section_header.dart';
import '../../widgets/small_spinner.dart';
class InfoScreen extends StatefulWidget {
const InfoScreen({super.key});
@@ -68,7 +69,7 @@ class _InfoScreenState extends State<InfoScreen> {
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: _info == null
? Center(
child: Text(
@@ -8,6 +8,7 @@ import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/section_header.dart';
import '../../widgets/settings_card.dart';
import '../../widgets/small_spinner.dart';
class NotificationsScreen extends StatefulWidget {
const NotificationsScreen({super.key});
@@ -101,7 +102,7 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
body: SafeArea(
top: false,
child: _loading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
@@ -7,6 +7,7 @@ import '../../../l10n/app_localizations.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/primary_loading_button.dart';
import '../../widgets/small_spinner.dart';
class PasswordEntryScreen extends StatefulWidget {
const PasswordEntryScreen({super.key});
@@ -142,7 +143,7 @@ class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
if (_isLoading) {
return Scaffold(
backgroundColor: cs.surface,
body: Center(child: CircularProgressIndicator(color: cs.primary)),
body: Center(child: SmallSpinner(size: 36, color: cs.primary)),
);
}
@@ -12,6 +12,7 @@ import '../../widgets/custom_notification.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import 'password_entry_screen.dart';
class SecurityScreen extends StatefulWidget {
@@ -219,14 +220,7 @@ class _SecurityScreenState extends State<SecurityScreen>
if (_isSaving)
Padding(
padding: const EdgeInsets.only(right: 16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.primary,
),
),
child: SmallSpinner(size: 20, color: cs.primary),
),
],
),
@@ -17,6 +17,7 @@ import '../../widgets/info_action_sheet.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/settings_card.dart';
import '../../widgets/sheet_helpers.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/custom_notification.dart';
import '../auth/login_screen.dart';
import '../auth/proxy_settings_sheet.dart';
@@ -222,7 +223,7 @@ class _SettingsTabState extends State<SettingsTab> {
final cs = Theme.of(context).colorScheme;
if (_profile == null) {
return const Center(child: CircularProgressIndicator());
return const Center(child: SmallSpinner(size: 36));
}
final String fullName =
@@ -18,6 +18,7 @@ import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/info_action_sheet.dart';
import '../../widgets/section_header.dart';
import '../../widgets/small_spinner.dart';
import '../auth/login_screen.dart';
enum SpoofingMethod { partial, full }
@@ -444,7 +445,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: SmallSpinner(size: 36))
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
child: Column(
@@ -14,6 +14,7 @@ import '../../../models/story.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/liquid_glass.dart';
import '../../widgets/small_spinner.dart';
import 'story_owner_info.dart';
const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮'];
@@ -490,14 +491,7 @@ class _StoryViewerScreenState extends State<StoryViewerScreen>
const _TopScrim(),
if (loading)
const Center(
child: SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: Colors.white,
),
),
child: SmallSpinner(size: 28, color: Colors.white),
),
SafeArea(
child: Column(
@@ -1069,14 +1063,7 @@ class _OwnerCover extends StatelessWidget {
imageUrl: info?.avatarUrl,
),
const SizedBox(height: 14),
const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white30,
),
),
const SmallSpinner(size: 22, color: Colors.white30),
],
),
),
@@ -9,6 +9,7 @@ import '../../../core/storage/spoofing_service.dart';
import '../../../main.dart' show api;
import '../../widgets/connection_status.dart';
import '../../widgets/error_view.dart';
import '../../widgets/small_spinner.dart';
import '../../widgets/webview_permission_prompt.dart';
class WebAppScreen extends StatefulWidget {
@@ -137,7 +138,7 @@ class _WebAppScreenState extends State<WebAppScreen> {
}
final launch = _launch;
if (launch == null) {
return const Center(child: CircularProgressIndicator());
return const Center(child: SmallSpinner(size: 36));
}
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(launch.url)),
@@ -16,6 +16,8 @@ import 'package:komet/frontend/widgets/sheet_helpers.dart';
import 'package:komet/frontend/widgets/sliding_pill_nav.dart';
import 'package:komet/l10n/app_localizations.dart';
import '../small_spinner.dart';
const int _navItemCount = 5;
List<PillNavItem> _buildNavItems(AppLocalizations l10n) => [
@@ -395,7 +397,7 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
double bottomReserve,
) {
if (_loading) {
return Center(child: CircularProgressIndicator(color: cs.primary));
return Center(child: SmallSpinner(size: 36, color: cs.primary));
}
if (_permission == GalleryPermission.denied) {
return _buildDenied(scrollController, cs, bottomReserve);
@@ -10,6 +10,7 @@ import '../../../../core/utils/haptics.dart';
import '../../../../core/utils/logger.dart';
import '../../../../core/utils/media_cache.dart';
import '../../../../models/attachment.dart';
import '../../small_spinner.dart';
class VideoNoteBubble extends StatefulWidget {
final VideoAttachment attachment;
@@ -182,10 +183,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble> {
child: _loading
? const Padding(
padding: EdgeInsets.all(14),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
child: SmallSpinner(size: 36, color: Colors.white),
)
: Icon(
_error ? Symbols.error : Symbols.play_arrow,
@@ -13,6 +13,7 @@ import '../../../../core/utils/format.dart';
import '../../../../core/utils/logger.dart';
import '../../../../core/utils/media_cache.dart';
import '../../custom_notification.dart';
import '../../small_spinner.dart';
class VoiceMessageBubble extends StatefulWidget {
final int duration;
@@ -231,8 +232,8 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
child: _loadingAudio
? Padding(
padding: const EdgeInsets.all(8),
child: CircularProgressIndicator(
strokeWidth: 2,
child: SmallSpinner(
size: 36,
color: widget.isMe
? widget.cs.onPrimaryContainer
: widget.cs.primary,
@@ -273,13 +274,9 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
height: 32,
child: Center(
child: _transcriptionLoading
? SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: widget.textColor.withValues(alpha: 0.6),
),
? SmallSpinner(
size: 12,
color: widget.textColor.withValues(alpha: 0.6),
)
: Text(
'Т',
@@ -10,6 +10,7 @@ import 'package:komet/frontend/widgets/attachment/photo_editor.dart';
import 'package:komet/frontend/widgets/custom_notification.dart';
import '../../../core/config/app_colors.dart';
import '../small_spinner.dart';
const Color _kBar = Color(0xFF1E1E1E);
@@ -218,11 +219,7 @@ class _MediaPreviewScreenState extends State<MediaPreviewScreen> {
Widget _buildImage() {
final file = _workingFile;
if (file == null) {
return const SizedBox(
width: 36,
height: 36,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white24),
);
return const SmallSpinner(size: 36, color: Colors.white24);
}
return Image.file(file, fit: BoxFit.contain, gaplessPlayback: true);
}
@@ -405,7 +405,7 @@ class _PhotoCropEditorState extends State<PhotoCropEditor> {
final img = _image;
if (img == null) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
child: SmallSpinner(size: 36, color: Colors.white),
);
}
return LayoutBuilder(
@@ -2350,7 +2350,7 @@ class _PhotoAdjustEditorState extends State<PhotoAdjustEditor> {
final img = _image;
if (img == null) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
child: SmallSpinner(size: 36, color: Colors.white),
);
}
return LayoutBuilder(
@@ -6,6 +6,7 @@ import '../../backend/modules/contacts.dart';
import '../../core/utils/media_saver.dart';
import '../../main.dart';
import 'custom_notification.dart';
import 'small_spinner.dart';
class AvatarHistoryScreen extends StatefulWidget {
final int contactId;
@@ -210,14 +211,7 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
Expanded(child: _buildCounter()),
IconButton(
icon: _saving
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: Colors.white,
),
)
? const SmallSpinner(size: 22, color: Colors.white)
: const Icon(Symbols.download, color: Colors.white),
onPressed: _pages.isEmpty || _saving ? null : _save,
),
@@ -276,7 +270,7 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
if (_pages.isEmpty) {
return Center(
child: _loading
? const CircularProgressIndicator(color: Colors.white)
? const SmallSpinner(size: 36, color: Colors.white)
: const Text(
'Нет фотографий',
style: TextStyle(color: Colors.white54, fontSize: 15),
@@ -293,7 +287,7 @@ class _AvatarHistoryScreenState extends State<AvatarHistoryScreen> {
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 120),
placeholder: (_, _) => const Center(
child: CircularProgressIndicator(color: Colors.white),
child: SmallSpinner(size: 36, color: Colors.white),
),
errorWidget: (_, _, _) =>
const Icon(Symbols.broken_image, color: Colors.white54, size: 64),
@@ -23,6 +23,7 @@ import '../../screens/chats/chat_screen.dart';
import '../custom_notification.dart';
import '../komet_avatar.dart';
import '../photo_viewer.dart';
import '../small_spinner.dart';
import '../swipe_route.dart';
import '../video_player_screen.dart';
@@ -103,11 +104,7 @@ Widget _loadingState(ColorScheme cs) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 56),
child: Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2.5, color: cs.primary),
),
child: SmallSpinner(size: 26, color: cs.primary),
),
);
}
@@ -611,14 +608,7 @@ class _SharedMediaTabState extends State<SharedMediaTab> {
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: _loadingMore
? SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: cs.primary,
),
)
? SmallSpinner(size: 22, color: cs.primary)
: TextButton(
onPressed: _loadMore,
child: Text(l10n.sharedLoadMore),
@@ -1174,10 +1164,7 @@ class _ProfileVoiceTileState extends State<_ProfileVoiceTile> {
child: _loadingAudio
? const Padding(
padding: EdgeInsets.all(13),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
child: SmallSpinner(size: 36, color: Colors.white),
)
: ValueListenableBuilder<double>(
valueListenable: _progress,
+2 -8
View File
@@ -5,6 +5,7 @@ import 'package:m3e_collection/m3e_collection.dart';
import '../../backend/api.dart';
import '../../main.dart' show api;
import 'small_spinner.dart';
final ValueNotifier<bool> debugForceOffline = ValueNotifier<bool>(false);
@@ -175,14 +176,7 @@ class ConnectionSpinner extends StatelessWidget {
],
),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: cs.primary,
),
),
child: SmallSpinner(size: 20, color: cs.primary),
),
),
),
@@ -14,6 +14,7 @@ import '../../l10n/app_localizations.dart';
import 'custom_notification.dart';
import 'komet_avatar.dart';
import 'lottie_image.dart';
import 'small_spinner.dart';
class ReactionEmoji {
final String emoji;
@@ -1227,11 +1228,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
body = const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2.4),
),
child: SmallSpinner(size: 24),
),
);
} else {
@@ -1314,11 +1311,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
body = const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2.4),
),
child: SmallSpinner(size: 24),
),
);
} else {
@@ -1647,11 +1640,7 @@ class _ReactionEmojiPickerState extends State<_ReactionEmojiPicker> {
Expanded(
child: !_loaded
? const Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2.4),
),
child: SmallSpinner(size: 26),
)
: _results.isEmpty
? const SizedBox.shrink()
+3 -9
View File
@@ -21,6 +21,7 @@ import '../../main.dart';
import '../../models/attachment.dart';
import 'chat_menu_overlay.dart';
import 'custom_notification.dart';
import 'small_spinner.dart';
class PhotoViewerActions {
final void Function(String messageId, int time)? goToMessage;
@@ -550,14 +551,7 @@ class _PhotoViewerScreenState extends State<PhotoViewerScreen> {
Expanded(child: _buildInfo(l10n)),
IconButton(
icon: _saving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
? const SmallSpinner(size: 20, color: Colors.white)
: const Icon(Symbols.download, color: Colors.white),
onPressed: _saving ? null : _save,
tooltip: l10n.sharedDownload,
@@ -666,7 +660,7 @@ class _PhotoViewerScreenState extends State<PhotoViewerScreen> {
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 120),
placeholder: (_, _) =>
const Center(child: CircularProgressIndicator(color: Colors.white)),
const Center(child: SmallSpinner(size: 36, color: Colors.white)),
errorWidget: (_, _, _) => _broken(),
);
}
+11 -22
View File
@@ -6,6 +6,7 @@ import '../../core/utils/format.dart';
import '../../core/utils/haptics.dart';
import '../../models/poll.dart';
import 'custom_notification.dart';
import 'small_spinner.dart';
class PollView extends StatefulWidget {
final int chatId;
@@ -222,14 +223,7 @@ class _PollViewState extends State<PollView>
),
),
if (_voting && !multiple)
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: widget.dimColor,
),
),
SmallSpinner(size: 14, color: widget.dimColor),
],
),
),
@@ -253,14 +247,7 @@ class _PollViewState extends State<PollView>
),
),
child: _voting
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: widget.accentColor,
),
)
? SmallSpinner(size: 16, color: widget.accentColor)
: const Text('Проголосовать'),
),
),
@@ -355,12 +342,14 @@ class _PollViewState extends State<PollView>
padding: const EdgeInsets.only(top: 6),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: fillFactor,
minHeight: 6,
backgroundColor: widget.dimColor.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(
widget.accentColor,
child: Container(
height: 6,
color: widget.dimColor.withValues(alpha: 0.2),
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: fillFactor.clamp(0.0, 1.0),
heightFactor: 1,
child: ColoredBox(color: widget.accentColor),
),
),
),
@@ -1,6 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'small_spinner.dart';
import 'springy_tap.dart';
class PrimaryLoadingButton extends StatelessWidget {
final ValueListenable<bool> loading;
final VoidCallback? onPressed;
@@ -23,23 +26,22 @@ class PrimaryLoadingButton extends StatelessWidget {
final fg = foreground ?? cs.onPrimary;
return ValueListenableBuilder<bool>(
valueListenable: loading,
builder: (context, isLoading, _) => FilledButton(
onPressed: isLoading ? null : onPressed,
style: FilledButton.styleFrom(
backgroundColor: background ?? cs.primary,
foregroundColor: fg,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
builder: (context, isLoading, _) => SpringyTap(
enabled: !isLoading,
child: FilledButton(
onPressed: isLoading ? null : onPressed,
style: FilledButton.styleFrom(
backgroundColor: background ?? cs.primary,
foregroundColor: fg,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: isLoading
? SmallSpinner(size: 20, color: fg)
: child,
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: fg),
)
: child,
),
);
}
+8 -6
View File
@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
class SmallSpinner extends StatelessWidget {
final double size;
final double strokeWidth;
final Color? color;
const SmallSpinner({
super.key,
this.size = 26,
this.strokeWidth = 2.4,
this.color,
});
@@ -17,9 +16,12 @@ class SmallSpinner extends StatelessWidget {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: strokeWidth,
color: color ?? Theme.of(context).colorScheme.primary,
child: FittedBox(
fit: BoxFit.contain,
child: ExpressiveLoadingIndicator(
color: color ?? Theme.of(context).colorScheme.primary,
constraints: BoxConstraints.tight(const Size.square(48)),
),
),
);
}
@@ -33,7 +35,7 @@ class BusyOverlay extends StatelessWidget {
return const Positioned.fill(
child: ColoredBox(
color: Colors.black54,
child: Center(child: CircularProgressIndicator(color: Colors.white)),
child: Center(child: SmallSpinner(size: 44, color: Colors.white)),
),
);
}
+108
View File
@@ -0,0 +1,108 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class SpringyTap extends StatefulWidget {
final Widget child;
final double pressedScale;
final bool enabled;
const SpringyTap({
super.key,
required this.child,
this.pressedScale = 0.98,
this.enabled = true,
});
@override
State<SpringyTap> createState() => _SpringyTapState();
}
class _SpringyTapState extends State<SpringyTap>
with SingleTickerProviderStateMixin {
static const Duration _pressDelay = Duration(milliseconds: 60);
static const Duration _pressDuration = Duration(milliseconds: 90);
static final SpringDescription _spring = SpringDescription.withDampingRatio(
ratio: 0.8,
stiffness: 350,
mass: 1,
);
late final AnimationController _controller = AnimationController.unbounded(
vsync: this,
value: 1.0,
);
Timer? _pressTimer;
@override
void dispose() {
_pressTimer?.cancel();
_controller.dispose();
super.dispose();
}
void _press() {
_controller.stop();
_controller.animateTo(
widget.pressedScale,
duration: _pressDuration,
curve: Curves.easeOut,
);
}
void _release() {
if (_controller.value == 1.0 && !_controller.isAnimating) return;
_controller.animateWith(
SpringSimulation(_spring, _controller.value, 1.0, 1.5),
);
}
Future<void> _pulse() async {
_controller.stop();
await _controller.animateTo(
widget.pressedScale,
duration: _pressDuration,
curve: Curves.easeOut,
);
if (mounted) _release();
}
void _onDown(PointerDownEvent _) {
_pressTimer?.cancel();
_pressTimer = Timer(_pressDelay, () {
_pressTimer = null;
_press();
});
}
void _onUp(PointerUpEvent _) {
if (_pressTimer != null) {
_pressTimer!.cancel();
_pressTimer = null;
unawaited(_pulse());
} else {
_release();
}
}
void _onCancel(PointerCancelEvent _) {
_pressTimer?.cancel();
_pressTimer = null;
_release();
}
@override
Widget build(BuildContext context) {
if (!widget.enabled) return widget.child;
return Listener(
behavior: HitTestBehavior.deferToChild,
onPointerDown: _onDown,
onPointerUp: _onUp,
onPointerCancel: _onCancel,
child: ScaleTransition(scale: _controller, child: widget.child),
);
}
}
+3 -7
View File
@@ -310,13 +310,9 @@ class _StickerPackSheetState extends State<_StickerPackSheet> {
),
onPressed: _busy ? null : _toggle,
child: _busy
? SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: _isFavorite ? cs.onSurface : cs.onPrimary,
),
? SmallSpinner(
size: 22,
color: _isFavorite ? cs.onSurface : cs.onPrimary,
)
: Text(
_isFavorite ? 'Убрать' : 'Добавить',
@@ -3,6 +3,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:video_player/video_player.dart';
import '../../core/utils/format.dart';
import 'small_spinner.dart';
class VideoPlayerScreen extends StatefulWidget {
final Map<String, String> sources;
@@ -133,11 +134,11 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
aspectRatio: c.value.aspectRatio,
child: VideoPlayer(c),
)
: const CircularProgressIndicator(color: Colors.white),
: const SmallSpinner(size: 36, color: Colors.white),
),
if (buffering)
const Center(
child: CircularProgressIndicator(color: Colors.white),
child: SmallSpinner(size: 36, color: Colors.white),
),
if (!_error)
AnimatedOpacity(
+2 -1
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../main.dart' show accountModule;
import 'custom_notification.dart';
import 'sheet_helpers.dart';
import 'small_spinner.dart';
Future<bool> showWebQrLoginConfirmSheet(BuildContext context) async {
final agreed = await showModalBottomSheet<bool>(
@@ -88,7 +89,7 @@ Future<bool> confirmAndAuthorizeWebQrLogin(
color: cs.surfaceContainerHigh,
child: const Padding(
padding: EdgeInsets.all(28),
child: CircularProgressIndicator(),
child: SmallSpinner(size: 36),
),
),
),
+7 -1
View File
@@ -80,6 +80,7 @@ import 'frontend/screens/auth/login_screen.dart';
import 'frontend/widgets/adaptive_shell.dart';
import 'frontend/widgets/custom_notification.dart';
import 'frontend/widgets/liquid_glass.dart';
import 'frontend/widgets/small_spinner.dart';
import 'frontend/widgets/theme_reveal.dart';
final api = Api();
@@ -98,6 +99,9 @@ final RouteObserver<PageRoute<dynamic>> appRouteObserver =
bool isOnemeFlavor = false;
const ProgressIndicatorThemeData _expressiveProgressTheme =
ProgressIndicatorThemeData(year2023: false);
const PageTransitionsTheme _appPageTransitions = PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
@@ -835,6 +839,7 @@ class KometAppState extends State<KometApp>
useMaterial3: true,
colorScheme: light,
pageTransitionsTheme: _appPageTransitions,
progressIndicatorTheme: _expressiveProgressTheme,
textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.light).textTheme,
@@ -846,6 +851,7 @@ class KometAppState extends State<KometApp>
useMaterial3: true,
colorScheme: dark,
pageTransitionsTheme: _appPageTransitions,
progressIndicatorTheme: _expressiveProgressTheme,
textTheme: AppFonts.textTheme(
_fontId,
ThemeData(brightness: Brightness.dark).textTheme,
@@ -1064,7 +1070,7 @@ class _StartupScreenState extends State<_StartupScreen> {
return Scaffold(
backgroundColor: cs.surface,
body: Center(
child: CircularProgressIndicator(color: cs.primary, strokeWidth: 2),
child: SmallSpinner(size: 36, color: cs.primary),
),
);
}
+1 -1
View File
@@ -262,7 +262,7 @@ SPEC CHECKSUMS:
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
record_macos: 5d55909f9650314be6424ffd6b123ac75a08c3c1
rlottie: c2780aabf858ca3c81785f09256827ca83b0765d
rlottie: 206daeeeb0f9dec6594ed248eb0c7e876a289e93
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0