удаление чатов и фикс загрузки ботов в chatsэ

This commit is contained in:
Jganenok
2026-05-17 18:29:28 +07:00
parent 74bad21635
commit 3aa6cdb52c
6 changed files with 841 additions and 364 deletions
@@ -8,6 +8,7 @@ import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'chat_screen.dart';
import 'create_group_flow.dart';
import '../../widgets/custom_notification.dart';
import '../calls/calls_tab.dart';
import '../contacts/contacts_tab.dart';
@@ -61,6 +62,8 @@ class ChatListScreen extends StatefulWidget {
State<ChatListScreen> createState() => _ChatListScreenState();
}
enum _DeleteKind { personalLike, ownerGroup, blocked }
class _ChatListScreenState extends State<ChatListScreen>
with TickerProviderStateMixin {
String? _selectedFolderId;
@@ -168,6 +171,176 @@ class _ChatListScreenState extends State<ChatListScreen>
});
}
List<CachedChat> _selectedChatObjects() {
if (_selectedChats.isEmpty) return const [];
final ids = _selectedChats;
return _chats.where((c) => ids.contains(c.id.toString())).toList();
}
_DeleteKind _categorizeChat(CachedChat c, int myId) {
if (c.type == 'DIALOG') return _DeleteKind.personalLike;
if (c.iAmAdmin(myId)) return _DeleteKind.ownerGroup;
return _DeleteKind.blocked;
}
_DeleteKind? _selectionDeleteCategory() {
if (_sessionState != SessionState.online) return null;
final myId = _profile?.id;
if (myId == null) return null;
final selected = _selectedChatObjects();
if (selected.isEmpty) return null;
final cats = selected.map((c) => _categorizeChat(c, myId)).toSet();
if (cats.contains(_DeleteKind.blocked)) return null;
if (cats.length > 1) return null;
return cats.single;
}
Future<void> _onDeleteTap() async {
final selectedBefore = _selectedChatObjects();
if (selectedBefore.isEmpty) return;
final myId = _profile?.id;
if (myId == null) return;
await ChatsModule.refreshChats(api, selectedBefore.map((c) => c.id).toList());
if (!mounted) return;
final selectedAfter = _selectedChatObjects();
if (selectedAfter.isEmpty) return;
final cats = selectedAfter.map((c) => _categorizeChat(c, myId)).toSet();
if (cats.contains(_DeleteKind.blocked) || cats.length > 1) {
showCustomNotification(context, 'Статус чатов изменился, попробуйте ещё раз');
return;
}
final kind = cats.single;
final confirmed = await _showDeleteConfirmDialog(selectedAfter, kind);
if (!mounted || confirmed != true) return;
final errors = <String>[];
for (final c in selectedAfter) {
final forAll = kind == _DeleteKind.ownerGroup;
final err = await ChatsModule.deleteChat(
api,
chatId: c.id,
lastEventTime: c.lastEventTime,
forAll: forAll,
);
if (err != null) errors.add(err);
}
if (!mounted) return;
if (errors.isNotEmpty) {
final msg = errors.length == 1
? errors.first
: 'Не удалось удалить ${errors.length} чат(ов): ${errors.first}';
showCustomNotification(context, msg);
}
_clearSelection();
}
Future<bool?> _showDeleteConfirmDialog(
List<CachedChat> selected,
_DeleteKind kind,
) {
final cs = Theme.of(context).colorScheme;
final count = selected.length;
final single = count == 1 ? selected.first : null;
String title;
String body;
String primaryLabel;
switch (kind) {
case _DeleteKind.personalLike:
title = single != null
? 'Удалить чат с ${single.title ?? ''}?'
: 'Удалить $count чатов?';
body = 'Восстановить переписку не получится';
primaryLabel = count == 1 ? 'Удалить чат' : 'Удалить';
case _DeleteKind.ownerGroup:
title = single != null
? 'Хотите удалить чат «${single.title ?? ''}»?'
: 'Удалить $count групп у всех?';
body = single != null
? 'Передайте права владельца, чтобы остальные участники могли продолжить общение'
: 'Действие нельзя отменить';
primaryLabel = count == 1 ? 'Удалить чат у всех' : 'Удалить у всех';
case _DeleteKind.blocked:
return Future.value(false);
}
return showModalBottomSheet<bool>(
context: context,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
body,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
const SizedBox(height: 20),
if (kind == _DeleteKind.ownerGroup && single != null) ...[
Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(22),
),
child: Text(
'Передать права и выйти',
style: TextStyle(
color: cs.onSurface.withValues(alpha: 0.4),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
],
GestureDetector(
onTap: () => Navigator.pop(ctx, true),
child: Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: cs.error,
borderRadius: BorderRadius.circular(22),
),
child: Text(
primaryLabel,
style: TextStyle(
color: cs.onError,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
},
);
}
bool _isInitialLoading = true;
DateTime _storiesLockdownUntil = DateTime.fromMillisecondsSinceEpoch(0);
@@ -393,7 +566,9 @@ class _ChatListScreenState extends State<ChatListScreen>
if (!mounted) return;
_contactRebuildTimer?.cancel();
_contactRebuildTimer = Timer(const Duration(milliseconds: 120), () {
if (mounted) setState(() {});
if (!mounted) return;
_cachedChatsBody = null;
setState(() {});
});
}
@@ -1644,10 +1819,11 @@ class _ChatListScreenState extends State<ChatListScreen>
),
),
const Spacer(),
IconButton(
icon: Icon(Symbols.delete, color: cs.onSurface),
onPressed: () {},
),
if (_selectionDeleteCategory() != null)
IconButton(
icon: Icon(Symbols.delete, color: cs.onSurface),
onPressed: _onDeleteTap,
),
IconButton(
icon: Icon(Symbols.archive, color: cs.onSurface),
onPressed: () {},
+342 -340
View File
@@ -12,19 +12,10 @@ import '../../../main.dart';
import '../../widgets/custom_notification.dart';
import 'chat_screen.dart';
const int _maxAvatarBytes = 8 * 1024 * 1024;
Future<void> showCreateGroupFlow(BuildContext context) async {
final cs = Theme.of(context).colorScheme;
final selected = await showModalBottomSheet<List<CachedContact>>(
context: context,
isScrollControlled: true,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (_) => const _ParticipantsPickerSheet(),
);
if (selected == null) return;
if (!context.mounted) return;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
@@ -32,30 +23,43 @@ Future<void> showCreateGroupFlow(BuildContext context) async {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (_) => _GroupDetailsSheet(participants: selected),
builder: (_) => const _CreateGroupFlow(),
);
}
class _ParticipantsPickerSheet extends StatefulWidget {
const _ParticipantsPickerSheet();
enum _Step { pickParticipants, groupDetails }
class _CreateGroupFlow extends StatefulWidget {
const _CreateGroupFlow();
@override
State<_ParticipantsPickerSheet> createState() => _ParticipantsPickerSheetState();
State<_CreateGroupFlow> createState() => _CreateGroupFlowState();
}
class _ParticipantsPickerSheetState extends State<_ParticipantsPickerSheet> {
final TextEditingController _search = TextEditingController();
class _CreateGroupFlowState extends State<_CreateGroupFlow> {
_Step _step = _Step.pickParticipants;
List<CachedContact> _all = [];
final Set<int> _selectedIds = {};
final List<CachedContact> _selected = [];
bool _loading = true;
final TextEditingController _search = TextEditingController();
final TextEditingController _title = TextEditingController();
File? _avatar;
bool _creating = false;
@override
void initState() {
super.initState();
_load();
_loadContacts();
}
Future<void> _load() async {
@override
void dispose() {
_search.dispose();
_title.dispose();
super.dispose();
}
Future<void> _loadContacts() async {
try {
final myId = await TokenStorage.getActiveAccountId();
if (myId == null) {
@@ -75,223 +79,18 @@ class _ParticipantsPickerSheetState extends State<_ParticipantsPickerSheet> {
}
}
String _displayName(CachedContact c) {
final last = c.lastName ?? '';
return last.isEmpty ? c.firstName : '${c.firstName} $last';
void _toggle(CachedContact c) {
setState(() {
final idx = _selected.indexWhere((x) => x.id == c.id);
if (idx >= 0) {
_selected.removeAt(idx);
} else {
_selected.add(c);
}
});
}
String _statusText(CachedContact c) {
if (c.isBot) return 'Бот';
return 'Был(-а) недавно';
}
@override
void dispose() {
_search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final viewInsets = MediaQuery.of(context).viewInsets;
final query = _search.text.trim().toLowerCase();
final filtered = query.isEmpty
? _all
: _all.where((c) => _displayName(c).toLowerCase().contains(query)).toList();
final selected = _all.where((c) => _selectedIds.contains(c.id)).toList();
return Padding(
padding: EdgeInsets.only(bottom: viewInsets.bottom),
child: SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 12, 8),
child: Row(
children: [
Expanded(
child: Text(
'Выберите участников',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.close, color: cs.onSurfaceVariant),
),
],
),
),
if (selected.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final c in selected)
_SelectedChip(
contact: c,
label: _displayName(c),
onRemove: () => setState(() => _selectedIds.remove(c.id)),
cs: cs,
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: TextField(
controller: _search,
onChanged: (_) => setState(() {}),
style: TextStyle(color: cs.onSurface, fontSize: 14),
decoration: InputDecoration(
hintText: 'Найти по имени',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
prefixIcon: Icon(Symbols.search, color: cs.onSurfaceVariant, size: 20),
isDense: true,
border: InputBorder.none,
),
),
),
Flexible(
child: _loading
? const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator()),
)
: ListView.builder(
padding: EdgeInsets.zero,
itemCount: filtered.length,
itemBuilder: (ctx, i) {
final c = filtered[i];
final picked = _selectedIds.contains(c.id);
final dim = c.isBot;
return InkWell(
onTap: () {
setState(() {
if (picked) {
_selectedIds.remove(c.id);
} else {
_selectedIds.add(c.id);
}
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
_Avatar(contact: c, size: 40, cs: cs),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_displayName(c),
style: TextStyle(
color: dim
? cs.onSurface.withValues(alpha: 0.5)
: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_statusText(c),
style: TextStyle(
color: cs.onSurfaceVariant.withValues(alpha: 0.8),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (picked)
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: cs.primary,
shape: BoxShape.circle,
),
child: Icon(Symbols.check, color: cs.onPrimary, size: 16),
),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
children: [
Expanded(
child: _SheetButton(
label: 'Отменить',
filled: false,
onTap: () => Navigator.pop(context),
cs: cs,
),
),
const SizedBox(width: 12),
Expanded(
child: _SheetButton(
label: 'Далее',
filled: true,
onTap: () {
final picked = _all
.where((c) => _selectedIds.contains(c.id))
.toList();
Navigator.pop(context, picked);
},
cs: cs,
),
),
],
),
),
],
),
),
),
);
}
}
class _GroupDetailsSheet extends StatefulWidget {
final List<CachedContact> participants;
const _GroupDetailsSheet({required this.participants});
@override
State<_GroupDetailsSheet> createState() => _GroupDetailsSheetState();
}
class _GroupDetailsSheetState extends State<_GroupDetailsSheet> {
final TextEditingController _title = TextEditingController();
File? _avatar;
bool _creating = false;
@override
void dispose() {
_title.dispose();
super.dispose();
}
bool _isSelected(int id) => _selected.any((c) => c.id == id);
Future<void> _pickAvatar() async {
if (_creating) return;
@@ -299,18 +98,27 @@ class _GroupDetailsSheetState extends State<_GroupDetailsSheet> {
if (result == null || result.files.isEmpty) return;
final path = result.files.first.path;
if (path == null) return;
setState(() => _avatar = File(path));
final file = File(path);
final size = await file.length();
if (size > _maxAvatarBytes) {
if (!mounted) return;
showCustomNotification(context, 'Картинка слишком большая (макс 8 МБ)');
return;
}
if (!mounted) return;
setState(() => _avatar = file);
}
Future<void> _create() async {
final title = _title.text.trim();
if (title.isEmpty || _creating) return;
setState(() => _creating = true);
final navigator = Navigator.of(context, rootNavigator: true);
try {
final chat = await ChatsModule.createGroupChat(
api,
title: title,
userIds: widget.participants.map((c) => c.id).toList(),
userIds: _selected.map((c) => c.id).toList(),
);
if (!mounted) return;
if (chat == null) {
@@ -323,17 +131,22 @@ class _GroupDetailsSheetState extends State<_GroupDetailsSheet> {
final url = await ChatsModule.requestChatPhotoUploadUrl(api);
if (url != null) {
final bytes = await _avatar!.readAsBytes();
final token = await fileUploader.uploadImage(Uri.parse(url), bytes);
final token = await fileUploader.uploadImage(
Uri.parse(url),
bytes,
filename: _avatar!.uri.pathSegments.last,
);
if (token != null) {
await ChatsModule.setChatPhoto(api, chatId: chat.id, photoToken: token);
} else if (mounted) {
showCustomNotification(context, 'Не удалось загрузить аватарку');
}
}
}
if (!mounted) return;
Navigator.pop(context);
Navigator.push(
context,
navigator.pop();
navigator.push(
MaterialPageRoute(
builder: (_) => ChatScreen(
chatId: chat.id,
@@ -351,113 +164,304 @@ class _GroupDetailsSheetState extends State<_GroupDetailsSheet> {
}
}
String _displayName(CachedContact c) {
final last = c.lastName ?? '';
return last.isEmpty ? c.firstName : '${c.firstName} $last';
}
String _statusText(CachedContact c) => c.isBot ? 'Бот' : 'Был(-а) недавно';
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final viewInsets = MediaQuery.of(context).viewInsets;
final canCreate = _title.text.trim().isNotEmpty && !_creating;
return Padding(
padding: EdgeInsets.only(bottom: viewInsets.bottom),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 4),
child: Row(
children: [
IconButton(
onPressed: _creating ? null : () => Navigator.pop(context),
icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, anim) {
final offset = child.key == const ValueKey(_Step.pickParticipants)
? Offset(-0.05, 0)
: Offset(0.05, 0);
return SlideTransition(
position: Tween<Offset>(begin: offset, end: Offset.zero).animate(anim),
child: FadeTransition(opacity: anim, child: child),
);
},
child: _step == _Step.pickParticipants
? KeyedSubtree(
key: const ValueKey(_Step.pickParticipants),
child: _buildPickerStep(),
)
: KeyedSubtree(
key: const ValueKey(_Step.groupDetails),
child: _buildDetailsStep(),
),
Expanded(
child: Text(
'Создать группу',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: _creating
? null
: () {
Navigator.pop(context);
Navigator.maybePop(context);
},
icon: Icon(Symbols.close, color: cs.onSurfaceVariant),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
children: [
GestureDetector(
onTap: _pickAvatar,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
clipBehavior: Clip.antiAlias,
child: _avatar != null
? Image.file(_avatar!, fit: BoxFit.cover)
: Icon(Symbols.add_a_photo, color: cs.onSurfaceVariant, size: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _title,
onChanged: (_) => setState(() {}),
enabled: !_creating,
style: TextStyle(color: cs.onSurface, fontSize: 16),
decoration: InputDecoration(
hintText: 'Название группы',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 16),
border: InputBorder.none,
isDense: true,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Expanded(
child: _SheetButton(
label: 'Отменить',
filled: false,
onTap: _creating ? null : () => Navigator.pop(context),
cs: cs,
),
),
const SizedBox(width: 12),
Expanded(
child: _SheetButton(
label: _creating ? 'Создаю...' : 'Создать',
filled: true,
onTap: canCreate ? _create : null,
cs: cs,
),
),
],
),
),
],
),
),
),
);
}
Widget _buildPickerStep() {
final cs = Theme.of(context).colorScheme;
final query = _search.text.trim().toLowerCase();
final filtered = query.isEmpty
? _all
: _all.where((c) => _displayName(c).toLowerCase().contains(query)).toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 12, 8),
child: Row(
children: [
Expanded(
child: Text(
'Выберите участников',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.close, color: cs.onSurfaceVariant),
),
],
),
),
if (_selected.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final c in _selected)
_SelectedChip(
contact: c,
label: _displayName(c),
onRemove: () => _toggle(c),
cs: cs,
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: TextField(
controller: _search,
onChanged: (_) => setState(() {}),
style: TextStyle(color: cs.onSurface, fontSize: 14),
decoration: InputDecoration(
hintText: 'Найти по имени',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
prefixIcon: Icon(Symbols.search, color: cs.onSurfaceVariant, size: 20),
isDense: true,
border: InputBorder.none,
),
),
),
Flexible(
child: _loading
? const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator()),
)
: ListView.builder(
padding: EdgeInsets.zero,
itemCount: filtered.length,
itemBuilder: (ctx, i) {
final c = filtered[i];
final picked = _isSelected(c.id);
final dim = c.isBot;
return InkWell(
onTap: () => _toggle(c),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
_Avatar(contact: c, size: 40, cs: cs),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_displayName(c),
style: TextStyle(
color: dim
? cs.onSurface.withValues(alpha: 0.5)
: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_statusText(c),
style: TextStyle(
color: cs.onSurfaceVariant.withValues(alpha: 0.8),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (picked)
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: cs.primary,
shape: BoxShape.circle,
),
child: Icon(Symbols.check, color: cs.onPrimary, size: 16),
),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
children: [
Expanded(
child: _SheetButton(
label: 'Отменить',
filled: false,
onTap: () => Navigator.pop(context),
cs: cs,
),
),
const SizedBox(width: 12),
Expanded(
child: _SheetButton(
label: 'Далее',
filled: true,
onTap: () => setState(() => _step = _Step.groupDetails),
cs: cs,
),
),
],
),
),
],
);
}
Widget _buildDetailsStep() {
final cs = Theme.of(context).colorScheme;
final canCreate = _title.text.trim().isNotEmpty && !_creating;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 4),
child: Row(
children: [
IconButton(
onPressed: _creating
? null
: () => setState(() => _step = _Step.pickParticipants),
icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant),
),
Expanded(
child: Text(
'Создать группу',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: _creating ? null : () => Navigator.pop(context),
icon: Icon(Symbols.close, color: cs.onSurfaceVariant),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
children: [
GestureDetector(
onTap: _pickAvatar,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
clipBehavior: Clip.antiAlias,
child: _avatar != null
? Image.file(_avatar!, fit: BoxFit.cover)
: Icon(Symbols.add_a_photo, color: cs.onSurfaceVariant, size: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _title,
onChanged: (_) => setState(() {}),
enabled: !_creating,
style: TextStyle(color: cs.onSurface, fontSize: 16),
decoration: InputDecoration(
hintText: 'Название группы',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 16),
border: InputBorder.none,
isDense: true,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Expanded(
child: _SheetButton(
label: 'Отменить',
filled: false,
onTap: _creating ? null : () => Navigator.pop(context),
cs: cs,
),
),
const SizedBox(width: 12),
Expanded(
child: _SheetButton(
label: _creating ? 'Создаю...' : 'Создать',
filled: true,
onTap: canCreate ? _create : null,
cs: cs,
),
),
],
),
),
],
);
}
}
class _Avatar extends StatelessWidget {
@@ -570,9 +574,7 @@ class _SheetButton extends StatelessWidget {
alignment: Alignment.center,
decoration: BoxDecoration(
color: filled
? (disabled
? cs.primary.withValues(alpha: 0.4)
: cs.primary)
? (disabled ? cs.primary.withValues(alpha: 0.4) : cs.primary)
: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(22),
),