refactor(useragent::evm::table): broke down into more widgets
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'package:arbiter/features/connection/evm.dart';
|
||||
import 'package:arbiter/features/connection/evm.dart' as evm;
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'evm.g.dart';
|
||||
@@ -14,7 +16,7 @@ class Evm extends _$Evm {
|
||||
return null;
|
||||
}
|
||||
|
||||
return listEvmWallets(connection);
|
||||
return evm.listEvmWallets(connection);
|
||||
}
|
||||
|
||||
Future<void> refreshWallets() async {
|
||||
@@ -25,16 +27,21 @@ class Evm extends _$Evm {
|
||||
}
|
||||
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => listEvmWallets(connection));
|
||||
state = await AsyncValue.guard(() => evm.listEvmWallets(connection));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createWallet() async {
|
||||
final connection = await ref.read(connectionManagerProvider.future);
|
||||
final createEvmWallet = Mutation();
|
||||
|
||||
Future<void> executeCreateEvmWallet(MutationTarget target) async {
|
||||
return await createEvmWallet.run(target, (tsx) async {
|
||||
final connection = await tsx.get(connectionManagerProvider.future);
|
||||
if (connection == null) {
|
||||
throw Exception('Not connected to the server.');
|
||||
}
|
||||
|
||||
await createEvmWallet(connection);
|
||||
state = await AsyncValue.guard(() => listEvmWallets(connection));
|
||||
}
|
||||
await evm.createEvmWallet(connection);
|
||||
|
||||
await tsx.get(evmProvider.notifier).refreshWallets();
|
||||
});
|
||||
}
|
||||
@@ -33,7 +33,7 @@ final class EvmProvider
|
||||
Evm create() => Evm();
|
||||
}
|
||||
|
||||
String _$evmHash() => r'f5d05bfa7b820d0b96026a47ca47702a3793af5d';
|
||||
String _$evmHash() => r'ca2c9736065c5dc7cc45d8485000dd85dfbfa572';
|
||||
|
||||
abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> {
|
||||
FutureOr<List<WalletEntry>?> build();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/screens/dashboard/evm/wallets/header.dart';
|
||||
import 'package:arbiter/screens/dashboard/evm/wallets/table.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||
import 'package:arbiter/providers/evm/evm.dart';
|
||||
import 'package:arbiter/widgets/page_header.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
@@ -17,13 +15,10 @@ class EvmScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final walletsAsync = ref.watch(evmProvider);
|
||||
final isCreating = useState(false);
|
||||
final evm = ref.watch(evmProvider);
|
||||
|
||||
final wallets = walletsAsync.asData?.value;
|
||||
final wallets = evm.asData?.value;
|
||||
final loadedWallets = wallets ?? const <WalletEntry>[];
|
||||
final isConnected =
|
||||
ref.watch(connectionManagerProvider).asData?.value != null;
|
||||
|
||||
void showMessage(String message) {
|
||||
if (!context.mounted) return;
|
||||
@@ -35,28 +30,12 @@ class EvmScreen extends HookConsumerWidget {
|
||||
Future<void> refreshWallets() async {
|
||||
try {
|
||||
await ref.read(evmProvider.notifier).refreshWallets();
|
||||
} catch (error) {
|
||||
showMessage(_formatError(error));
|
||||
} catch (e) {
|
||||
showMessage('Failed to refresh wallets: ${_formatError(e)}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createWallet() async {
|
||||
if (isCreating.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
try {
|
||||
await ref.read(evmProvider.notifier).createWallet();
|
||||
showMessage('Wallet created.');
|
||||
} catch (error) {
|
||||
showMessage(_formatError(error));
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
final content = switch (walletsAsync) {
|
||||
final content = switch (evm) {
|
||||
AsyncLoading() when wallets == null => const _StatePanel(
|
||||
icon: Icons.hourglass_top,
|
||||
title: 'Loading wallets',
|
||||
@@ -70,22 +49,14 @@ class EvmScreen extends HookConsumerWidget {
|
||||
actionLabel: 'Retry',
|
||||
onAction: refreshWallets,
|
||||
),
|
||||
_ when !isConnected => _StatePanel(
|
||||
AsyncData(:final value) when value == null => _StatePanel(
|
||||
icon: Icons.portable_wifi_off,
|
||||
title: 'No active server connection',
|
||||
body: 'Reconnect to Arbiter to list or create EVM wallets.',
|
||||
actionLabel: 'Refresh',
|
||||
onAction: refreshWallets,
|
||||
onAction: () => refreshWallets(),
|
||||
),
|
||||
_ when loadedWallets.isEmpty => _StatePanel(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
title: 'No wallets yet',
|
||||
body:
|
||||
'Create the first vault-backed wallet to start building your EVM registry.',
|
||||
actionLabel: isCreating.value ? 'Creating...' : 'Create wallet',
|
||||
onAction: isCreating.value ? null : createWallet,
|
||||
),
|
||||
_ => _WalletTable(wallets: loadedWallets),
|
||||
_ => WalletTable(wallets: loadedWallets),
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
@@ -102,47 +73,11 @@ class EvmScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'EVM Wallet Vault',
|
||||
isBusy: walletsAsync.isLoading,
|
||||
isBusy: evm.isLoading,
|
||||
actions: [
|
||||
FilledButton.icon(
|
||||
onPressed: isCreating.value ? null : () => createWallet(),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Palette.ink,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.4.w,
|
||||
vertical: 1.2.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: isCreating.value
|
||||
? SizedBox(
|
||||
width: 1.6.h,
|
||||
height: 1.6.h,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.2),
|
||||
)
|
||||
: const Icon(Icons.add_circle_outline, size: 18),
|
||||
label: Text(isCreating.value ? 'Creating...' : 'Create'),
|
||||
),
|
||||
const CreateWalletButton(),
|
||||
SizedBox(width: 1.w),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => refreshWallets(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Palette.ink,
|
||||
side: BorderSide(color: Palette.line),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.4.w,
|
||||
vertical: 1.2.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Refresh'),
|
||||
),
|
||||
const RefreshWalletButton(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 1.8.h),
|
||||
@@ -155,197 +90,6 @@ class EvmScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
double get _accentStripWidth => 0.8.w;
|
||||
double get _cellHorizontalPadding => 1.8.w;
|
||||
double get _walletColumnWidth => 18.w;
|
||||
double get _columnGap => 1.8.w;
|
||||
double get _tableMinWidth => 72.w;
|
||||
|
||||
class _WalletTable extends StatelessWidget {
|
||||
const _WalletTable({required this.wallets});
|
||||
|
||||
final List<WalletEntry> wallets;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: Palette.cream.withValues(alpha: 0.92),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.h),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Managed wallets',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.6.h),
|
||||
Text(
|
||||
'Every address here is generated and held by Arbiter.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.70),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.6.h),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: tableWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
const _WalletTableHeader(),
|
||||
SizedBox(height: 1.h),
|
||||
for (var i = 0; i < wallets.length; i++)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: i == wallets.length - 1 ? 0 : 1.h,
|
||||
),
|
||||
child: _WalletTableRow(
|
||||
wallet: wallets[i],
|
||||
index: i,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTableHeader extends StatelessWidget {
|
||||
const _WalletTableHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.72),
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.3,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 1.4.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Palette.ink.withValues(alpha: 0.04),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: _accentStripWidth + _cellHorizontalPadding),
|
||||
SizedBox(
|
||||
width: _walletColumnWidth,
|
||||
child: Text('Wallet', style: style),
|
||||
),
|
||||
SizedBox(width: _columnGap),
|
||||
Expanded(child: Text('Address', style: style)),
|
||||
SizedBox(width: _cellHorizontalPadding),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTableRow extends StatelessWidget {
|
||||
const _WalletTableRow({required this.wallet, required this.index});
|
||||
|
||||
final WalletEntry wallet;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = _accentColor(wallet.address);
|
||||
final address = _hexAddress(wallet.address);
|
||||
final rowHeight = 5.h;
|
||||
final walletStyle = Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.copyWith(color: Palette.ink);
|
||||
final addressStyle = Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Palette.ink);
|
||||
|
||||
return Container(
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: accent.withValues(alpha: 0.10),
|
||||
border: Border.all(color: accent.withValues(alpha: 0.28)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: _accentStripWidth,
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: _cellHorizontalPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _walletColumnWidth,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 1.2.h,
|
||||
height: 1.2.h,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 1.w),
|
||||
Text(
|
||||
'Wallet ${(index + 1).toString().padLeft(2, '0')}',
|
||||
style: walletStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: _columnGap),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: addressStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatePanel extends StatelessWidget {
|
||||
const _StatePanel({
|
||||
required this.icon,
|
||||
@@ -417,19 +161,6 @@ class _StatePanel extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _hexAddress(List<int> bytes) {
|
||||
final hex = bytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
return '0x$hex';
|
||||
}
|
||||
|
||||
Color _accentColor(List<int> bytes) {
|
||||
final seed = bytes.fold<int>(0, (value, byte) => value + byte);
|
||||
final hue = (seed * 17) % 360;
|
||||
return HSLColor.fromAHSL(1, hue.toDouble(), 0.68, 0.54).toColor();
|
||||
}
|
||||
|
||||
String _formatError(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Exception: ')) {
|
||||
|
||||
98
useragent/lib/screens/dashboard/evm/wallets/header.dart
Normal file
98
useragent/lib/screens/dashboard/evm/wallets/header.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:arbiter/providers/evm/evm.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
|
||||
class CreateWalletButton extends ConsumerWidget {
|
||||
const CreateWalletButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final createWallet = ref.watch(createEvmWallet);
|
||||
final isCreating = createWallet is MutationPending;
|
||||
|
||||
Future<void> handleCreateWallet() async {
|
||||
try {
|
||||
await executeCreateEvmWallet(ref);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('New wallet created successfully.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to create wallet: ${_formatError(e)}'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return FilledButton.icon(
|
||||
onPressed: isCreating ? null : () => handleCreateWallet(),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Palette.ink,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.4.w, vertical: 1.2.h),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
icon: isCreating
|
||||
? SizedBox(
|
||||
width: 1.6.h,
|
||||
height: 1.6.h,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.2),
|
||||
)
|
||||
: const Icon(Icons.add_circle_outline, size: 18),
|
||||
label: Text(isCreating ? 'Creating...' : 'Create'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RefreshWalletButton extends ConsumerWidget {
|
||||
const RefreshWalletButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> handleRefreshWallets() async {
|
||||
try {
|
||||
await ref.read(evmProvider.notifier).refreshWallets();
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to refresh wallets: ${_formatError(e)}'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return OutlinedButton.icon(
|
||||
onPressed: () => handleRefreshWallets(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Palette.ink,
|
||||
side: BorderSide(color: Palette.line),
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.4.w, vertical: 1.2.h),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Refresh'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String _formatError(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Exception: ')) {
|
||||
return message.substring('Exception: '.length);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
209
useragent/lib/screens/dashboard/evm/wallets/table.dart
Normal file
209
useragent/lib/screens/dashboard/evm/wallets/table.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
double get _accentStripWidth => 0.8.w;
|
||||
double get _cellHorizontalPadding => 1.8.w;
|
||||
double get _walletColumnWidth => 18.w;
|
||||
double get _columnGap => 1.8.w;
|
||||
double get _tableMinWidth => 72.w;
|
||||
|
||||
String _hexAddress(List<int> bytes) {
|
||||
final hex = bytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
return '0x$hex';
|
||||
}
|
||||
|
||||
Color _accentColor(List<int> bytes) {
|
||||
final seed = bytes.fold<int>(0, (value, byte) => value + byte);
|
||||
final hue = (seed * 17) % 360;
|
||||
return HSLColor.fromAHSL(1, hue.toDouble(), 0.68, 0.54).toColor();
|
||||
}
|
||||
|
||||
class WalletTable extends StatelessWidget {
|
||||
const WalletTable({super.key, required this.wallets});
|
||||
|
||||
final List<WalletEntry> wallets;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: Palette.cream.withValues(alpha: 0.92),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.h),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Managed wallets',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 0.6.h),
|
||||
Text(
|
||||
'Every address here is generated and held by Arbiter.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.70),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.6.h),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: tableWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
const _WalletTableHeader(),
|
||||
SizedBox(height: 1.h),
|
||||
for (var i = 0; i < wallets.length; i++)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: i == wallets.length - 1 ? 0 : 1.h,
|
||||
),
|
||||
child: _WalletTableRow(
|
||||
wallet: wallets[i],
|
||||
index: i,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTableHeader extends StatelessWidget {
|
||||
const _WalletTableHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.72),
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.3,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 1.4.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Palette.ink.withValues(alpha: 0.04),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: _accentStripWidth + _cellHorizontalPadding),
|
||||
SizedBox(
|
||||
width: _walletColumnWidth,
|
||||
child: Text('Wallet', style: style),
|
||||
),
|
||||
SizedBox(width: _columnGap),
|
||||
Expanded(child: Text('Address', style: style)),
|
||||
SizedBox(width: _cellHorizontalPadding),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTableRow extends StatelessWidget {
|
||||
const _WalletTableRow({required this.wallet, required this.index});
|
||||
|
||||
final WalletEntry wallet;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = _accentColor(wallet.address);
|
||||
final address = _hexAddress(wallet.address);
|
||||
final rowHeight = 5.h;
|
||||
final walletStyle = Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.copyWith(color: Palette.ink);
|
||||
final addressStyle = Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Palette.ink);
|
||||
|
||||
return Container(
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: accent.withValues(alpha: 0.10),
|
||||
border: Border.all(color: accent.withValues(alpha: 0.28)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: _accentStripWidth,
|
||||
height: rowHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: _cellHorizontalPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _walletColumnWidth,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 1.2.h,
|
||||
height: 1.2.h,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 1.w),
|
||||
Text(
|
||||
'Wallet ${(index + 1).toString().padLeft(2, '0')}',
|
||||
style: walletStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: _columnGap),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: addressStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user