refactor(useragent::evm::table): broke down into more widgets
Some checks failed
ci/woodpecker/push/useragent-analyze Pipeline failed
Some checks failed
ci/woodpecker/push/useragent-analyze Pipeline failed
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/proto/evm.pb.dart';
|
||||||
import 'package:arbiter/providers/connection/connection_manager.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';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'evm.g.dart';
|
part 'evm.g.dart';
|
||||||
@@ -14,7 +16,7 @@ class Evm extends _$Evm {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return listEvmWallets(connection);
|
return evm.listEvmWallets(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshWallets() async {
|
Future<void> refreshWallets() async {
|
||||||
@@ -25,16 +27,21 @@ class Evm extends _$Evm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
state = await AsyncValue.guard(() => listEvmWallets(connection));
|
state = await AsyncValue.guard(() => evm.listEvmWallets(connection));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> createWallet() async {
|
final createEvmWallet = Mutation();
|
||||||
final connection = await ref.read(connectionManagerProvider.future);
|
|
||||||
|
Future<void> executeCreateEvmWallet(MutationTarget target) async {
|
||||||
|
return await createEvmWallet.run(target, (tsx) async {
|
||||||
|
final connection = await tsx.get(connectionManagerProvider.future);
|
||||||
if (connection == null) {
|
if (connection == null) {
|
||||||
throw Exception('Not connected to the server.');
|
throw Exception('Not connected to the server.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await createEvmWallet(connection);
|
await evm.createEvmWallet(connection);
|
||||||
state = await AsyncValue.guard(() => listEvmWallets(connection));
|
|
||||||
}
|
await tsx.get(evmProvider.notifier).refreshWallets();
|
||||||
}
|
});
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ final class EvmProvider
|
|||||||
Evm create() => Evm();
|
Evm create() => Evm();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$evmHash() => r'f5d05bfa7b820d0b96026a47ca47702a3793af5d';
|
String _$evmHash() => r'ca2c9736065c5dc7cc45d8485000dd85dfbfa572';
|
||||||
|
|
||||||
abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> {
|
abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> {
|
||||||
FutureOr<List<WalletEntry>?> build();
|
FutureOr<List<WalletEntry>?> build();
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:arbiter/proto/evm.pb.dart';
|
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/theme/palette.dart';
|
||||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
|
||||||
import 'package:arbiter/providers/evm/evm.dart';
|
import 'package:arbiter/providers/evm/evm.dart';
|
||||||
import 'package:arbiter/widgets/page_header.dart';
|
import 'package:arbiter/widgets/page_header.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:sizer/sizer.dart';
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
@@ -17,13 +15,10 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final walletsAsync = ref.watch(evmProvider);
|
final evm = ref.watch(evmProvider);
|
||||||
final isCreating = useState(false);
|
|
||||||
|
|
||||||
final wallets = walletsAsync.asData?.value;
|
final wallets = evm.asData?.value;
|
||||||
final loadedWallets = wallets ?? const <WalletEntry>[];
|
final loadedWallets = wallets ?? const <WalletEntry>[];
|
||||||
final isConnected =
|
|
||||||
ref.watch(connectionManagerProvider).asData?.value != null;
|
|
||||||
|
|
||||||
void showMessage(String message) {
|
void showMessage(String message) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -35,28 +30,12 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
Future<void> refreshWallets() async {
|
Future<void> refreshWallets() async {
|
||||||
try {
|
try {
|
||||||
await ref.read(evmProvider.notifier).refreshWallets();
|
await ref.read(evmProvider.notifier).refreshWallets();
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
showMessage(_formatError(error));
|
showMessage('Failed to refresh wallets: ${_formatError(e)}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createWallet() async {
|
final content = switch (evm) {
|
||||||
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) {
|
|
||||||
AsyncLoading() when wallets == null => const _StatePanel(
|
AsyncLoading() when wallets == null => const _StatePanel(
|
||||||
icon: Icons.hourglass_top,
|
icon: Icons.hourglass_top,
|
||||||
title: 'Loading wallets',
|
title: 'Loading wallets',
|
||||||
@@ -70,22 +49,14 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
actionLabel: 'Retry',
|
actionLabel: 'Retry',
|
||||||
onAction: refreshWallets,
|
onAction: refreshWallets,
|
||||||
),
|
),
|
||||||
_ when !isConnected => _StatePanel(
|
AsyncData(:final value) when value == null => _StatePanel(
|
||||||
icon: Icons.portable_wifi_off,
|
icon: Icons.portable_wifi_off,
|
||||||
title: 'No active server connection',
|
title: 'No active server connection',
|
||||||
body: 'Reconnect to Arbiter to list or create EVM wallets.',
|
body: 'Reconnect to Arbiter to list or create EVM wallets.',
|
||||||
actionLabel: 'Refresh',
|
actionLabel: 'Refresh',
|
||||||
onAction: refreshWallets,
|
onAction: () => refreshWallets(),
|
||||||
),
|
),
|
||||||
_ when loadedWallets.isEmpty => _StatePanel(
|
_ => WalletTable(wallets: loadedWallets),
|
||||||
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),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -102,47 +73,11 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
PageHeader(
|
PageHeader(
|
||||||
title: 'EVM Wallet Vault',
|
title: 'EVM Wallet Vault',
|
||||||
isBusy: walletsAsync.isLoading,
|
isBusy: evm.isLoading,
|
||||||
actions: [
|
actions: [
|
||||||
FilledButton.icon(
|
const CreateWalletButton(),
|
||||||
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'),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
SizedBox(width: 1.w),
|
||||||
OutlinedButton.icon(
|
const RefreshWalletButton(),
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 1.8.h),
|
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 {
|
class _StatePanel extends StatelessWidget {
|
||||||
const _StatePanel({
|
const _StatePanel({
|
||||||
required this.icon,
|
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) {
|
String _formatError(Object error) {
|
||||||
final message = error.toString();
|
final message = error.toString();
|
||||||
if (message.startsWith('Exception: ')) {
|
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