This commit is contained in:
2026-03-29 14:26:48 +03:00
parent 31dc972e52
commit 0020c65d1e
2 changed files with 233 additions and 193 deletions
+111 -89
View File
@@ -37,6 +37,7 @@ class AddTransactionScreen extends ConsumerStatefulWidget {
class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _stackKey = GlobalKey();
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _noteController = TextEditingController(); final _noteController = TextEditingController();
final _fromAccountIndicatorKey = GlobalKey(); final _fromAccountIndicatorKey = GlobalKey();
@@ -507,6 +508,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Stack( child: Stack(
key: _stackKey,
children: [ children: [
ListView( ListView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@@ -685,6 +687,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
onClose: () => onClose: () =>
setState(() => _showFromAccountDropdown = false), setState(() => _showFromAccountDropdown = false),
triggerKey: _fromAccountIndicatorKey, triggerKey: _fromAccountIndicatorKey,
stackKey: _stackKey,
), ),
if (_showToAccountDropdown) if (_showToAccountDropdown)
Positioned.fill( Positioned.fill(
@@ -699,6 +702,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
initial: widget.initial, initial: widget.initial,
onClose: () => setState(() => _showToAccountDropdown = false), onClose: () => setState(() => _showToAccountDropdown = false),
triggerKey: _toAccountIndicatorKey, triggerKey: _toAccountIndicatorKey,
stackKey: _stackKey,
), ),
], ],
), ),
@@ -839,11 +843,13 @@ class _ToAccountDropdownOverlay extends ConsumerWidget {
final Transaction? initial; final Transaction? initial;
final VoidCallback onClose; final VoidCallback onClose;
final GlobalKey? triggerKey; final GlobalKey? triggerKey;
final GlobalKey? stackKey;
const _ToAccountDropdownOverlay({ const _ToAccountDropdownOverlay({
required this.initial, required this.initial,
required this.onClose, required this.onClose,
this.triggerKey, this.triggerKey,
this.stackKey,
}); });
@override @override
@@ -857,106 +863,122 @@ class _ToAccountDropdownOverlay extends ConsumerWidget {
// Calculate position from trigger key // Calculate position from trigger key
double top = 340; double top = 340;
double left = 20; double left = 20;
double triggerWidth = 200; // fallback width
if (triggerKey?.currentContext != null) { if (triggerKey?.currentContext != null) {
final renderBox = final triggerBox =
triggerKey!.currentContext!.findRenderObject() as RenderBox; triggerKey!.currentContext!.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero); final triggerOffset = triggerBox.localToGlobal(Offset.zero);
final size = renderBox.size; final triggerSize = triggerBox.size;
top = offset.dy + size.height + 4; triggerWidth = triggerSize.width;
left = offset.dx;
double stackDy = 0;
double stackDx = 0;
if (stackKey?.currentContext != null) {
final stackBox =
stackKey!.currentContext!.findRenderObject() as RenderBox;
final stackOffset = stackBox.localToGlobal(Offset.zero);
stackDy = stackOffset.dy;
stackDx = stackOffset.dx;
}
top = triggerOffset.dy - stackDy + triggerSize.height + 4;
left = triggerOffset.dx - stackDx;
} }
return Positioned( return Positioned(
top: top, top: top,
left: left, left: left,
child: IntrinsicWidth( width: triggerWidth,
child: Material( child: Material(
elevation: 8, elevation: 8,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: const Color(0xFF7C6DED).withOpacity(0.3), color: const Color(0xFF7C6DED).withOpacity(0.3),
width: 1.5, width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
), ),
boxShadow: [ ],
BoxShadow( ),
color: Colors.black.withOpacity(0.2), child: accountsAsync.when(
blurRadius: 16, data: (accounts) {
offset: const Offset(0, 4), final filteredAccounts = accounts
), .where((a) => a.id != selectedAccountId)
], .toList();
),
child: accountsAsync.when(
data: (accounts) {
final filteredAccounts = accounts
.where((a) => a.id != selectedAccountId)
.toList();
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: filteredAccounts.map((account) { children: filteredAccounts.map((account) {
final isSelected = account.id == toAccountId; final isSelected = account.id == toAccountId;
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: () { onTap: () {
ref ref
.read(addTransactionProvider(initial).notifier) .read(addTransactionProvider(initial).notifier)
.setToAccountId(account.id); .setToAccountId(account.id);
onClose(); onClose();
HapticService.light(); HapticService.light();
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 14, horizontal: 14,
vertical: 12, vertical: 12,
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_rounded,
size: 16,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 10),
Expanded(
child: Text(
account.name,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(context).colorScheme.onSurface,
),
),
),
if (isSelected)
const Icon(
Icons.check_rounded,
size: 16,
color: Color(0xFF7C6DED),
),
],
),
), ),
); child: Row(
}).toList(), children: [
); Icon(
}, Icons.account_balance_wallet_rounded,
loading: () => const SizedBox.shrink(), size: 16,
error: (_, __) => const SizedBox.shrink(), color: isSelected
), ? const Color(0xFF7C6DED)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 10),
Expanded(
child: Text(
account.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(context).colorScheme.onSurface,
),
),
),
if (isSelected) ...[
const SizedBox(width: 10),
const Icon(
Icons.check_rounded,
size: 16,
color: Color(0xFF7C6DED),
),
],
],
),
),
);
}).toList(),
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
), ),
), ),
), ),
@@ -120,12 +120,14 @@ class AccountDropdownOverlay extends ConsumerWidget {
final Transaction? initial; final Transaction? initial;
final VoidCallback onClose; final VoidCallback onClose;
final GlobalKey? triggerKey; final GlobalKey? triggerKey;
final GlobalKey? stackKey;
const AccountDropdownOverlay({ const AccountDropdownOverlay({
super.key, super.key,
required this.initial, required this.initial,
required this.onClose, required this.onClose,
this.triggerKey, this.triggerKey,
this.stackKey,
}); });
@override @override
@@ -136,123 +138,139 @@ class AccountDropdownOverlay extends ConsumerWidget {
// Calculate position from trigger key // Calculate position from trigger key
double top = 76; double top = 76;
double left = 20; double left = 20;
double triggerWidth = 200; // fallback width
if (triggerKey?.currentContext != null) { if (triggerKey?.currentContext != null) {
final renderBox = final triggerBox =
triggerKey!.currentContext!.findRenderObject() as RenderBox; triggerKey!.currentContext!.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero); final triggerOffset = triggerBox.localToGlobal(Offset.zero);
final size = renderBox.size; final triggerSize = triggerBox.size;
top = offset.dy + size.height + 4; triggerWidth = triggerSize.width;
left = offset.dx;
double stackDy = 0;
double stackDx = 0;
if (stackKey?.currentContext != null) {
final stackBox =
stackKey!.currentContext!.findRenderObject() as RenderBox;
final stackOffset = stackBox.localToGlobal(Offset.zero);
stackDy = stackOffset.dy;
stackDx = stackOffset.dx;
}
top = triggerOffset.dy - stackDy + triggerSize.height + 4;
left = triggerOffset.dx - stackDx;
} }
return Positioned( return Positioned(
top: top, top: top,
left: left, left: left,
child: IntrinsicWidth( width: triggerWidth,
child: Material( child: Material(
elevation: 8, elevation: 8,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: const Color(0xFF7C6DED).withOpacity(0.3), color: const Color(0xFF7C6DED).withOpacity(0.3),
width: 1.5, width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
), ),
child: accountsAsync.when( boxShadow: [
data: (accounts) { BoxShadow(
final txAccountId = ref color: Colors.black.withOpacity(0.2),
.read(addTransactionProvider(initial)) blurRadius: 16,
.selectedAccountId; offset: const Offset(0, 4),
final Account displayAccount; ),
if (txAccountId != null) { ],
displayAccount = accounts.firstWhere( ),
(a) => a.id == txAccountId, child: accountsAsync.when(
orElse: () => accounts.firstWhere( data: (accounts) {
final txAccountId = ref
.read(addTransactionProvider(initial))
.selectedAccountId;
final Account displayAccount;
if (txAccountId != null) {
displayAccount = accounts.firstWhere(
(a) => a.id == txAccountId,
orElse: () => accounts.firstWhere(
(a) => a.isMain,
orElse: () => accounts.first,
),
);
} else {
displayAccount =
activeAccount ??
accounts.firstWhere(
(a) => a.isMain, (a) => a.isMain,
orElse: () => accounts.first, orElse: () => accounts.first,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: accounts.map((account) {
final isSelected = account.id == displayAccount.id;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
ref
.read(addTransactionProvider(initial).notifier)
.setAccountId(account.id);
onClose();
HapticService.light();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_rounded,
size: 16,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 10),
Expanded(
child: Text(
account.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(context).colorScheme.onSurface,
),
),
),
if (isSelected) ...[
const SizedBox(width: 10),
const Icon(
Icons.check_rounded,
size: 16,
color: Color(0xFF7C6DED),
),
],
],
),
), ),
); );
} else { }).toList(),
displayAccount = );
activeAccount ?? },
accounts.firstWhere( loading: () => const SizedBox.shrink(),
(a) => a.isMain, error: (_, __) => const SizedBox.shrink(),
orElse: () => accounts.first,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: accounts.map((account) {
final isSelected = account.id == displayAccount.id;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
ref
.read(addTransactionProvider(initial).notifier)
.setAccountId(account.id);
onClose();
HapticService.light();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_rounded,
size: 16,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 10),
Expanded(
child: Text(
account.name,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(context).colorScheme.onSurface,
),
),
),
if (isSelected)
const Icon(
Icons.check_rounded,
size: 16,
color: Color(0xFF7C6DED),
),
],
),
),
);
}).toList(),
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
), ),
), ),
), ),