fix
This commit is contained in:
+110
-18
@@ -53,6 +53,7 @@ class CallLogEntry {
|
||||
final CallStatus status;
|
||||
final int time;
|
||||
final int count;
|
||||
final bool isGroup;
|
||||
|
||||
const CallLogEntry({
|
||||
required this.id,
|
||||
@@ -63,6 +64,7 @@ class CallLogEntry {
|
||||
required this.status,
|
||||
required this.time,
|
||||
this.count = 1,
|
||||
this.isGroup = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,22 +199,58 @@ class CallsModule {
|
||||
if (!response.isOk || response.payload is! Map) return [];
|
||||
|
||||
final payload = response.payload as Map<dynamic, dynamic>;
|
||||
return parseHistoryPayload(payload, accountId, currentUserId);
|
||||
return parseHistoryPayload(
|
||||
payload,
|
||||
accountId,
|
||||
currentUserId,
|
||||
resolver: resolveContacts,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<int, Map<String, dynamic>>> resolveContacts(List<int> ids) async {
|
||||
if (ids.isEmpty) return const {};
|
||||
final out = <int, Map<String, dynamic>>{};
|
||||
try {
|
||||
final resp = await _api.sendRequest(Opcode.contactInfo, {
|
||||
'contactIds': ids,
|
||||
});
|
||||
final data = resp.payload;
|
||||
final contacts = data is Map ? data['contacts'] : null;
|
||||
if (contacts is List) {
|
||||
for (final c in contacts) {
|
||||
if (c is Map) {
|
||||
final id = c['id'];
|
||||
if (id is int) out[id] = Map<String, dynamic>.from(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<bool> deleteHistory(List<int> historyIds) async {
|
||||
if (historyIds.isEmpty) return true;
|
||||
final response = await _api.sendRequest(Opcode.videoChatDeleteHistory, {
|
||||
'historyIds': historyIds,
|
||||
});
|
||||
return response.isOk;
|
||||
}
|
||||
|
||||
/// Парсинг истории звонков (opcode 79: videoChatHistory)
|
||||
static Future<List<CallLogEntry>> parseHistoryPayload(
|
||||
Map<dynamic, dynamic> payload,
|
||||
int accountId,
|
||||
int currentUserId,
|
||||
) async {
|
||||
int currentUserId, {
|
||||
Future<Map<int, Map<String, dynamic>>> Function(List<int> ids)? resolver,
|
||||
}) async {
|
||||
final history = payload['history'];
|
||||
if (history is! List || history.isEmpty) return [];
|
||||
|
||||
final recentContacts = await ContactsModule.getContacts(accountId);
|
||||
final contactsMap = {for (final c in recentContacts) c.id: c};
|
||||
|
||||
final List<CallLogEntry> extractedCalls = [];
|
||||
final parsed =
|
||||
<({int peerId, CallStatus status, int time, String id})>[];
|
||||
|
||||
for (final item in history.whereType<Map>()) {
|
||||
final msg = item['message'];
|
||||
@@ -241,26 +279,63 @@ class CallsModule {
|
||||
peerId = senderId;
|
||||
}
|
||||
|
||||
final contact = contactsMap[peerId];
|
||||
final status = _parseCallStatus(callAttach, isOutgoing);
|
||||
final time = (msg['time'] as int?) ?? 0;
|
||||
final msgId =
|
||||
msg['id']?.toString() ??
|
||||
DateTime.now().millisecondsSinceEpoch.toString();
|
||||
parsed.add((
|
||||
peerId: peerId,
|
||||
status: _parseCallStatus(callAttach, isOutgoing),
|
||||
time: (msg['time'] as int?) ?? 0,
|
||||
id:
|
||||
msg['id']?.toString() ??
|
||||
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
));
|
||||
}
|
||||
|
||||
final name = (contact != null && contact.firstName.isNotEmpty)
|
||||
? '${contact.firstName} ${contact.lastName ?? ''}'.trim()
|
||||
: 'Неизвестный';
|
||||
bool localResolved(int id) {
|
||||
final c = contactsMap[id];
|
||||
return c != null && c.firstName.isNotEmpty;
|
||||
}
|
||||
|
||||
final unresolvedIds = parsed
|
||||
.map((e) => e.peerId)
|
||||
.where((id) => id != 0 && !localResolved(id))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
var fetched = const <int, Map<String, dynamic>>{};
|
||||
if (unresolvedIds.isNotEmpty && resolver != null) {
|
||||
fetched = await resolver(unresolvedIds);
|
||||
}
|
||||
|
||||
final List<CallLogEntry> extractedCalls = [];
|
||||
for (final e in parsed) {
|
||||
final contact = contactsMap[e.peerId];
|
||||
String name;
|
||||
String? avatarUrl;
|
||||
bool isGroup = false;
|
||||
if (contact != null && contact.firstName.isNotEmpty) {
|
||||
name = '${contact.firstName} ${contact.lastName ?? ''}'.trim();
|
||||
avatarUrl = contact.baseUrl;
|
||||
} else {
|
||||
final info = fetched[e.peerId];
|
||||
final resolved = _nameFromInfo(info);
|
||||
if (resolved != null) {
|
||||
name = resolved;
|
||||
avatarUrl = (info?['baseUrl'] as String?) ?? contact?.baseUrl;
|
||||
} else {
|
||||
name = 'Групповой звонок';
|
||||
isGroup = true;
|
||||
}
|
||||
}
|
||||
|
||||
extractedCalls.add(
|
||||
CallLogEntry(
|
||||
id: msgId,
|
||||
id: e.id,
|
||||
accountId: accountId,
|
||||
peerId: peerId,
|
||||
peerId: e.peerId,
|
||||
name: name,
|
||||
avatarUrl: contact?.baseUrl,
|
||||
status: status,
|
||||
time: time,
|
||||
avatarUrl: avatarUrl,
|
||||
status: e.status,
|
||||
time: e.time,
|
||||
isGroup: isGroup,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -268,6 +343,23 @@ class CallsModule {
|
||||
return extractedCalls;
|
||||
}
|
||||
|
||||
static String? _nameFromInfo(Map<String, dynamic>? info) {
|
||||
if (info == null) return null;
|
||||
final names = info['names'];
|
||||
if (names is List && names.isNotEmpty) {
|
||||
final n = names.first;
|
||||
if (n is Map) {
|
||||
final full = n['name']?.toString();
|
||||
if (full != null && full.isNotEmpty) return full;
|
||||
final first = n['firstName']?.toString() ?? '';
|
||||
final last = n['lastName']?.toString() ?? '';
|
||||
final combined = '$first $last'.trim();
|
||||
if (combined.isNotEmpty) return combined;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static CallStatus _parseCallStatus(
|
||||
Map<dynamic, dynamic> callAttach,
|
||||
bool isOutgoing,
|
||||
|
||||
@@ -106,6 +106,7 @@ abstract class Opcode {
|
||||
static const int chatMembersUpdate = 77; // Обновление участников / добавление
|
||||
static const int videoChatStartActive = 78; // Инициация активного звонка
|
||||
static const int videoChatHistory = 79; // История звонков
|
||||
static const int videoChatDeleteHistory = 164; // Удаление записей истории звонков
|
||||
static const int videoChatCreateJoinLink = 84; // Ссылка для входа в видеочат
|
||||
static const int videoChatJoinByLink = 166; // Вход в звонок по ссылке
|
||||
static const int videoChatMembers = 195; // Участники видеочата
|
||||
@@ -288,6 +289,7 @@ abstract class Opcode {
|
||||
chatMembersUpdate: 'CHAT_MEMBERS_UPDATE',
|
||||
videoChatStartActive: 'VIDEO_CHAT_START_ACTIVE',
|
||||
videoChatHistory: 'VIDEO_CHAT_HISTORY',
|
||||
videoChatDeleteHistory: 'VIDEO_CHAT_DELETE_HISTORY',
|
||||
videoChatCreateJoinLink: 'VIDEO_CHAT_CREATE_JOIN_LINK',
|
||||
videoChatJoinByLink: 'VIDEO_CHAT_JOIN_BY_LINK',
|
||||
videoChatMembers: 'VIDEO_CHAT_MEMBERS',
|
||||
|
||||
@@ -6,10 +6,13 @@ import '../../../main.dart' show api, accountModule;
|
||||
import '../../../backend/modules/account.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/utils/format.dart';
|
||||
import '../../../core/calls/call_controller.dart';
|
||||
import '../../../backend/modules/calls.dart';
|
||||
import '../../widgets/komet_avatar.dart';
|
||||
import '../../widgets/connection_status.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/chat_menu_overlay.dart';
|
||||
import 'call_screen.dart';
|
||||
|
||||
class CallsTab extends StatefulWidget {
|
||||
const CallsTab({super.key});
|
||||
@@ -20,6 +23,7 @@ class CallsTab extends StatefulWidget {
|
||||
|
||||
class _CallsTabState extends State<CallsTab> {
|
||||
List<CallLogEntry> _calls = [];
|
||||
final Set<String> _removing = {};
|
||||
bool _isLoading = true;
|
||||
int _selectedTabIndex = 0; // 0 for 'Все', 1 for 'Пропущенные'
|
||||
StreamSubscription<LoginStatus>? _loginSub;
|
||||
@@ -79,6 +83,7 @@ class _CallsTabState extends State<CallsTab> {
|
||||
status: last.status,
|
||||
time: last.time,
|
||||
count: last.count + 1,
|
||||
isGroup: last.isGroup,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -154,16 +159,23 @@ class _CallsTabState extends State<CallsTab> {
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: call.isGroup ? cs.primaryContainer : null,
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: KometAvatar(
|
||||
name: call.name,
|
||||
imageUrl: call.avatarUrl,
|
||||
size: 48,
|
||||
),
|
||||
child: call.isGroup
|
||||
? Icon(
|
||||
Symbols.groups,
|
||||
color: cs.onPrimaryContainer,
|
||||
size: 26,
|
||||
)
|
||||
: KometAvatar(
|
||||
name: call.name,
|
||||
imageUrl: call.avatarUrl,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -244,17 +256,69 @@ class _CallsTabState extends State<CallsTab> {
|
||||
icon: Symbols.delete,
|
||||
label: 'Удалить',
|
||||
destructive: true,
|
||||
onTap: () {},
|
||||
),
|
||||
ChatMenuItem(
|
||||
icon: Symbols.call,
|
||||
label: 'Перезвонить',
|
||||
onTap: () {},
|
||||
onTap: () => _deleteCall(call),
|
||||
),
|
||||
if (!call.isGroup)
|
||||
ChatMenuItem(
|
||||
icon: Symbols.call,
|
||||
label: 'Перезвонить',
|
||||
onTap: () => _callBack(call),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteCall(CallLogEntry call) async {
|
||||
setState(() => _removing.add(call.id));
|
||||
final historyId = int.tryParse(call.id);
|
||||
if (historyId != null) {
|
||||
unawaited(CallsModule(api).deleteHistory([historyId]));
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 260));
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_calls.removeWhere((c) => c.id == call.id);
|
||||
_removing.remove(call.id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _callBack(CallLogEntry call) async {
|
||||
if (call.peerId <= 0) {
|
||||
showCustomNotification(context, 'Не удалось определить собеседника');
|
||||
return;
|
||||
}
|
||||
final navigator = Navigator.of(context);
|
||||
final avatarUrl = (call.avatarUrl?.isNotEmpty ?? false)
|
||||
? call.avatarUrl
|
||||
: null;
|
||||
final active = CallController.instance.activeSession;
|
||||
if (active != null) {
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
CallScreen(name: call.name, avatarUrl: avatarUrl, session: active),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final session = await CallController.instance.startOutgoing(call.peerId);
|
||||
if (!mounted) return;
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CallScreen(
|
||||
name: call.name,
|
||||
avatarUrl: avatarUrl,
|
||||
session: session,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
showCustomNotification(context, 'Не удалось начать звонок');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTabItem(String label, int index, ColorScheme cs) {
|
||||
final isSelected = _selectedTabIndex == index;
|
||||
return GestureDetector(
|
||||
@@ -369,10 +433,11 @@ class _CallsTabState extends State<CallsTab> {
|
||||
padding: const EdgeInsets.only(bottom: 120),
|
||||
itemCount: filteredCalls.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCallItem(
|
||||
context,
|
||||
cs,
|
||||
filteredCalls[index],
|
||||
final call = filteredCalls[index];
|
||||
return _RemovableCallEntry(
|
||||
key: ValueKey(call.id),
|
||||
removing: _removing.contains(call.id),
|
||||
child: _buildCallItem(context, cs, call),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -383,3 +448,51 @@ class _CallsTabState extends State<CallsTab> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RemovableCallEntry extends StatefulWidget {
|
||||
final bool removing;
|
||||
final Widget child;
|
||||
|
||||
const _RemovableCallEntry({
|
||||
required Key key,
|
||||
required this.removing,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_RemovableCallEntry> createState() => _RemovableCallEntryState();
|
||||
}
|
||||
|
||||
class _RemovableCallEntryState extends State<_RemovableCallEntry>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 260),
|
||||
value: 1.0,
|
||||
);
|
||||
late final Animation<double> _animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _RemovableCallEntry oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.removing && !oldWidget.removing) _controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizeTransition(
|
||||
sizeFactor: _animation,
|
||||
alignment: Alignment.topCenter,
|
||||
child: FadeTransition(opacity: _animation, child: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -1394,6 +1396,12 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: _SyncProbeCard(),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 120)),
|
||||
],
|
||||
),
|
||||
@@ -1402,6 +1410,178 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncProbeCard extends StatefulWidget {
|
||||
const _SyncProbeCard();
|
||||
|
||||
@override
|
||||
State<_SyncProbeCard> createState() => _SyncProbeCardState();
|
||||
}
|
||||
|
||||
class _SyncProbeCardState extends State<_SyncProbeCard> {
|
||||
final _phoneController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _result;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _send() async {
|
||||
final phone = _phoneController.text.trim();
|
||||
final name = _nameController.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
setState(() => _result = 'Введите номер');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_result = null;
|
||||
});
|
||||
try {
|
||||
final packet = await api.sendRequest(Opcode.sync, {
|
||||
'contactList': {
|
||||
phone: {'firstName': name},
|
||||
},
|
||||
});
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_result = _pretty(packet.payload);
|
||||
});
|
||||
} on PacketError catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_result = 'PacketError: ${e.message}';
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_result = 'Ошибка: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _pretty(dynamic payload) {
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
try {
|
||||
return encoder.convert(_jsonSafe(payload));
|
||||
} catch (_) {
|
||||
return payload.toString();
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _jsonSafe(dynamic v) {
|
||||
if (v is Map) {
|
||||
return v.map((k, val) => MapEntry(k.toString(), _jsonSafe(val)));
|
||||
}
|
||||
if (v is List) return v.map(_jsonSafe).toList();
|
||||
if (v is String || v is num || v is bool || v == null) return v;
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return GlossyPill(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
depth: 6,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sync contactList (21)',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Резолв контакта по номеру и имени, полный ответ сервера',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
enabled: !_loading,
|
||||
decoration: InputDecoration(
|
||||
hintText: '+6282233831826',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
enabled: !_loading,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Имя',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: _loading ? null : _send,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(44),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Отправить'),
|
||||
),
|
||||
if (_result != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: SelectableText(
|
||||
_result!,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _HitKind { dialog, chat, channel, bot, official, contact, user, unknown }
|
||||
|
||||
class _SearchHit {
|
||||
|
||||
@@ -154,8 +154,15 @@ class _AccountSwitcherLayerState extends State<_AccountSwitcherLayer>
|
||||
final totalItems = _accounts.length;
|
||||
final height = _vPad * 2 + totalItems * _itemHeight + _addItemHeight;
|
||||
|
||||
double menuX = widget.tapPoint.dx - _menuWidth / 2;
|
||||
menuX = menuX.clamp(_hMargin, screen.width - _menuWidth - _hMargin);
|
||||
final maxWidth = screen.width - 2 * _hMargin;
|
||||
final menuWidth = maxWidth <= 0
|
||||
? screen.width
|
||||
: (_menuWidth > maxWidth ? maxWidth : _menuWidth);
|
||||
|
||||
double menuX = widget.tapPoint.dx - menuWidth / 2;
|
||||
final maxX = screen.width - menuWidth - _hMargin;
|
||||
if (menuX > maxX) menuX = maxX;
|
||||
if (menuX < _hMargin) menuX = _hMargin;
|
||||
|
||||
final bottomInset = MediaQuery.viewPaddingOf(context).bottom;
|
||||
final maxBottom = screen.height - bottomInset - 88;
|
||||
@@ -166,19 +173,19 @@ class _AccountSwitcherLayerState extends State<_AccountSwitcherLayer>
|
||||
double menuY = menuBottom - height;
|
||||
if (menuY < 24) menuY = 24;
|
||||
|
||||
_menuRect = Rect.fromLTWH(menuX, menuY, _menuWidth, height);
|
||||
_menuRect = Rect.fromLTWH(menuX, menuY, menuWidth, height);
|
||||
_itemHitRects = [
|
||||
for (int i = 0; i < totalItems; i++)
|
||||
Rect.fromLTWH(
|
||||
menuX,
|
||||
menuY + _vPad + i * _itemHeight,
|
||||
_menuWidth,
|
||||
menuWidth,
|
||||
_itemHeight,
|
||||
),
|
||||
Rect.fromLTWH(
|
||||
menuX,
|
||||
menuY + _vPad + totalItems * _itemHeight,
|
||||
_menuWidth,
|
||||
menuWidth,
|
||||
_addItemHeight,
|
||||
),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user