refactor(useragent): moved shared CreamPanel and StatePanel into generic widgets
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
import 'package:arbiter/features/connection/evm/grants.dart';
|
import 'package:arbiter/features/connection/evm/grants.dart';
|
||||||
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:fixnum/fixnum.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||||
import 'package:mtcore/markettakers.dart';
|
import 'package:mtcore/markettakers.dart';
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'evm_grants.freezed.dart';
|
part 'evm_grants.freezed.dart';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:arbiter/proto/client.pb.dart';
|
import 'package:arbiter/proto/client.pb.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sizer/sizer.dart';
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
@@ -31,12 +32,7 @@ class SdkConnectCallout extends StatelessWidget {
|
|||||||
clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
|
clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
|
||||||
final showInfoCard = hasDescription || hasVersion;
|
final showInfoCard = hasDescription || hasVersion;
|
||||||
|
|
||||||
return Container(
|
return CreamFrame(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Palette.cream,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: Palette.line),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(2.4.h),
|
padding: EdgeInsets.all(2.4.h),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ClientDetailsStatePanel extends StatelessWidget {
|
class ClientDetailsStatePanel extends StatelessWidget {
|
||||||
@@ -17,15 +18,8 @@ class ClientDetailsStatePanel extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: CreamFrame(
|
||||||
padding: const EdgeInsets.all(24),
|
margin: const EdgeInsets.all(24),
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Palette.cream,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: Palette.line),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -38,8 +32,6 @@ class ClientDetailsStatePanel extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ClientSummaryCard extends StatelessWidget {
|
class ClientSummaryCard extends StatelessWidget {
|
||||||
@@ -9,13 +9,7 @@ class ClientSummaryCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DecoratedBox(
|
return CreamFrame(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Palette.cream,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: Palette.line),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -42,7 +36,6 @@ class ClientSummaryCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:arbiter/providers/sdk_clients/wallet_access.dart';
|
import 'package:arbiter/providers/sdk_clients/wallet_access.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||||
|
|
||||||
@@ -24,13 +25,7 @@ class WalletAccessSaveBar extends StatelessWidget {
|
|||||||
MutationError(:final error) => error.toString(),
|
MutationError(:final error) => error.toString(),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
return DecoratedBox(
|
return CreamFrame(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Palette.cream,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: Palette.line),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -54,7 +49,6 @@ class WalletAccessSaveBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:arbiter/providers/sdk_clients/wallet_access.dart';
|
|||||||
import 'package:arbiter/screens/dashboard/clients/details/widgets/client_details_state_panel.dart';
|
import 'package:arbiter/screens/dashboard/clients/details/widgets/client_details_state_panel.dart';
|
||||||
import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_list.dart';
|
import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_list.dart';
|
||||||
import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_search_field.dart';
|
import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_search_field.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
@@ -27,13 +27,7 @@ class WalletAccessSection extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final optionsAsync = ref.watch(clientWalletOptionsProvider);
|
final optionsAsync = ref.watch(clientWalletOptionsProvider);
|
||||||
return DecoratedBox(
|
return CreamFrame(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Palette.cream,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: Palette.line),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -56,7 +50,6 @@ class WalletAccessSection extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
|
import 'package:arbiter/widgets/state_panel.dart';
|
||||||
import 'package:sizer/sizer.dart';
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
// ─── Column width getters ─────────────────────────────────────────────────────
|
// ─── Column width getters ─────────────────────────────────────────────────────
|
||||||
@@ -59,79 +61,6 @@ String _formatError(Object error) {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── State panel ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class _StatePanel extends StatelessWidget {
|
|
||||||
const _StatePanel({
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.body,
|
|
||||||
this.actionLabel,
|
|
||||||
this.onAction,
|
|
||||||
this.busy = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String body;
|
|
||||||
final String? actionLabel;
|
|
||||||
final Future<void> Function()? onAction;
|
|
||||||
final bool busy;
|
|
||||||
|
|
||||||
@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.8.h),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (busy)
|
|
||||||
SizedBox(
|
|
||||||
width: 2.8.h,
|
|
||||||
height: 2.8.h,
|
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2.5),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Icon(icon, size: 34, color: Palette.coral),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
color: Palette.ink,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.h),
|
|
||||||
Text(
|
|
||||||
body,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: Palette.ink.withValues(alpha: 0.72),
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (actionLabel != null && onAction != null) ...[
|
|
||||||
SizedBox(height: 2.h),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => onAction!(),
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
label: Text(actionLabel!),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Header ───────────────────────────────────────────────────────────────────
|
// ─── Header ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _Header extends StatelessWidget {
|
class _Header extends StatelessWidget {
|
||||||
@@ -443,13 +372,7 @@ class _ClientTable extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Container(
|
return CreamFrame(
|
||||||
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),
|
padding: EdgeInsets.all(2.h),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -497,7 +420,6 @@ class _ClientTable extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,27 +455,27 @@ class ClientsScreen extends HookConsumerWidget {
|
|||||||
final clients = clientsAsync.asData?.value;
|
final clients = clientsAsync.asData?.value;
|
||||||
|
|
||||||
final content = switch (clientsAsync) {
|
final content = switch (clientsAsync) {
|
||||||
AsyncLoading() when clients == null => const _StatePanel(
|
AsyncLoading() when clients == null => const StatePanel(
|
||||||
icon: Icons.hourglass_top,
|
icon: Icons.hourglass_top,
|
||||||
title: 'Loading clients',
|
title: 'Loading clients',
|
||||||
body: 'Pulling client registry from Arbiter.',
|
body: 'Pulling client registry from Arbiter.',
|
||||||
busy: true,
|
busy: true,
|
||||||
),
|
),
|
||||||
AsyncError(:final error) => _StatePanel(
|
AsyncError(:final error) => StatePanel(
|
||||||
icon: Icons.sync_problem,
|
icon: Icons.sync_problem,
|
||||||
title: 'Client registry unavailable',
|
title: 'Client registry unavailable',
|
||||||
body: _formatError(error),
|
body: _formatError(error),
|
||||||
actionLabel: 'Retry',
|
actionLabel: 'Retry',
|
||||||
onAction: refresh,
|
onAction: refresh,
|
||||||
),
|
),
|
||||||
_ when !isConnected => _StatePanel(
|
_ when !isConnected => 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 SDK clients.',
|
body: 'Reconnect to Arbiter to list SDK clients.',
|
||||||
actionLabel: 'Refresh',
|
actionLabel: 'Refresh',
|
||||||
onAction: refresh,
|
onAction: refresh,
|
||||||
),
|
),
|
||||||
_ when clients != null && clients.isEmpty => _StatePanel(
|
_ when clients != null && clients.isEmpty => StatePanel(
|
||||||
icon: Icons.devices_other_outlined,
|
icon: Icons.devices_other_outlined,
|
||||||
title: 'No clients yet',
|
title: 'No clients yet',
|
||||||
body: 'SDK clients appear here once they register with Arbiter.',
|
body: 'SDK clients appear here once they register with Arbiter.',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/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:arbiter/widgets/state_panel.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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -36,20 +37,20 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final content = switch (evm) {
|
final content = switch (evm) {
|
||||||
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',
|
||||||
body: 'Pulling wallet registry from Arbiter.',
|
body: 'Pulling wallet registry from Arbiter.',
|
||||||
busy: true,
|
busy: true,
|
||||||
),
|
),
|
||||||
AsyncError(:final error) => _StatePanel(
|
AsyncError(:final error) => StatePanel(
|
||||||
icon: Icons.sync_problem,
|
icon: Icons.sync_problem,
|
||||||
title: 'Wallet registry unavailable',
|
title: 'Wallet registry unavailable',
|
||||||
body: _formatError(error),
|
body: _formatError(error),
|
||||||
actionLabel: 'Retry',
|
actionLabel: 'Retry',
|
||||||
onAction: refreshWallets,
|
onAction: refreshWallets,
|
||||||
),
|
),
|
||||||
AsyncData(:final value) when value == null => _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.',
|
||||||
@@ -90,77 +91,6 @@ class EvmScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StatePanel extends StatelessWidget {
|
|
||||||
const _StatePanel({
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.body,
|
|
||||||
this.actionLabel,
|
|
||||||
this.onAction,
|
|
||||||
this.busy = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String body;
|
|
||||||
final String? actionLabel;
|
|
||||||
final Future<void> Function()? onAction;
|
|
||||||
final bool busy;
|
|
||||||
|
|
||||||
@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.8.h),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (busy)
|
|
||||||
SizedBox(
|
|
||||||
width: 2.8.h,
|
|
||||||
height: 2.8.h,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Icon(icon, size: 34, color: Palette.coral),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
color: Palette.ink,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.h),
|
|
||||||
Text(
|
|
||||||
body,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: Palette.ink.withValues(alpha: 0.72),
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (actionLabel != null && onAction != null) ...[
|
|
||||||
SizedBox(height: 2.h),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => onAction!(),
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
label: Text(actionLabel!),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatError(Object error) {
|
String _formatError(Object error) {
|
||||||
final message = error.toString();
|
final message = error.toString();
|
||||||
if (message.startsWith('Exception: ')) {
|
if (message.startsWith('Exception: ')) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:arbiter/router.gr.dart';
|
|||||||
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
|
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
import 'package:arbiter/widgets/page_header.dart';
|
import 'package:arbiter/widgets/page_header.dart';
|
||||||
|
import 'package:arbiter/widgets/state_panel.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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -18,79 +19,6 @@ String _formatError(Object error) {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── State panel ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class _StatePanel extends StatelessWidget {
|
|
||||||
const _StatePanel({
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.body,
|
|
||||||
this.actionLabel,
|
|
||||||
this.onAction,
|
|
||||||
this.busy = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String body;
|
|
||||||
final String? actionLabel;
|
|
||||||
final Future<void> Function()? onAction;
|
|
||||||
final bool busy;
|
|
||||||
|
|
||||||
@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.8.h),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (busy)
|
|
||||||
SizedBox(
|
|
||||||
width: 2.8.h,
|
|
||||||
height: 2.8.h,
|
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2.5),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Icon(icon, size: 34, color: Palette.coral),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
color: Palette.ink,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.h),
|
|
||||||
Text(
|
|
||||||
body,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: Palette.ink.withValues(alpha: 0.72),
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (actionLabel != null && onAction != null) ...[
|
|
||||||
SizedBox(height: 2.h),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => onAction!(),
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
label: Text(actionLabel!),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Grant list ───────────────────────────────────────────────────────────────
|
// ─── Grant list ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _GrantList extends StatelessWidget {
|
class _GrantList extends StatelessWidget {
|
||||||
@@ -149,27 +77,27 @@ class EvmGrantsScreen extends ConsumerWidget {
|
|||||||
final grants = grantsState?.grants;
|
final grants = grantsState?.grants;
|
||||||
|
|
||||||
final content = switch (grantsAsync) {
|
final content = switch (grantsAsync) {
|
||||||
AsyncLoading() when grantsState == null => const _StatePanel(
|
AsyncLoading() when grantsState == null => const StatePanel(
|
||||||
icon: Icons.hourglass_top,
|
icon: Icons.hourglass_top,
|
||||||
title: 'Loading grants',
|
title: 'Loading grants',
|
||||||
body: 'Pulling grant registry from Arbiter.',
|
body: 'Pulling grant registry from Arbiter.',
|
||||||
busy: true,
|
busy: true,
|
||||||
),
|
),
|
||||||
AsyncError(:final error) => _StatePanel(
|
AsyncError(:final error) => StatePanel(
|
||||||
icon: Icons.sync_problem,
|
icon: Icons.sync_problem,
|
||||||
title: 'Grant registry unavailable',
|
title: 'Grant registry unavailable',
|
||||||
body: _formatError(error),
|
body: _formatError(error),
|
||||||
actionLabel: 'Retry',
|
actionLabel: 'Retry',
|
||||||
onAction: safeRefresh,
|
onAction: safeRefresh,
|
||||||
),
|
),
|
||||||
AsyncData(:final value) when value == null => _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 EVM grants.',
|
body: 'Reconnect to Arbiter to list EVM grants.',
|
||||||
actionLabel: 'Refresh',
|
actionLabel: 'Refresh',
|
||||||
onAction: safeRefresh,
|
onAction: safeRefresh,
|
||||||
),
|
),
|
||||||
_ when grants != null && grants.isEmpty => _StatePanel(
|
_ when grants != null && grants.isEmpty => StatePanel(
|
||||||
icon: Icons.policy_outlined,
|
icon: Icons.policy_outlined,
|
||||||
title: 'No grants yet',
|
title: 'No grants yet',
|
||||||
body: 'Create a grant to allow SDK clients to sign transactions.',
|
body: 'Create a grant to allow SDK clients to sign transactions.',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:arbiter/proto/evm.pb.dart';
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
import 'package:arbiter/theme/palette.dart';
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sizer/sizer.dart';
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
@@ -32,13 +33,7 @@ class WalletTable extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Container(
|
return CreamFrame(
|
||||||
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),
|
padding: EdgeInsets.all(2.h),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -89,7 +84,6 @@ class WalletTable extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
useragent/lib/widgets/cream_frame.dart
Normal file
32
useragent/lib/widgets/cream_frame.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A card-shaped frame with the cream background, rounded corners, and a
|
||||||
|
/// subtle border. Use [padding] for interior spacing and [margin] for exterior
|
||||||
|
/// spacing.
|
||||||
|
class CreamFrame extends StatelessWidget {
|
||||||
|
const CreamFrame({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.padding = EdgeInsets.zero,
|
||||||
|
this.margin,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final EdgeInsetsGeometry? margin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: margin,
|
||||||
|
padding: padding,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: Palette.cream,
|
||||||
|
border: Border.all(color: Palette.line),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
useragent/lib/widgets/state_panel.dart
Normal file
69
useragent/lib/widgets/state_panel.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:arbiter/widgets/cream_frame.dart';
|
||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
class StatePanel extends StatelessWidget {
|
||||||
|
const StatePanel({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
this.busy = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String? actionLabel;
|
||||||
|
final Future<void> Function()? onAction;
|
||||||
|
final bool busy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return CreamFrame(
|
||||||
|
padding: EdgeInsets.all(2.8.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (busy)
|
||||||
|
SizedBox(
|
||||||
|
width: 2.8.h,
|
||||||
|
height: 2.8.h,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(icon, size: 34, color: Palette.coral),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: Palette.ink,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.h),
|
||||||
|
Text(
|
||||||
|
body,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.72),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (actionLabel != null && onAction != null) ...[
|
||||||
|
SizedBox(height: 2.h),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => onAction!(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text(actionLabel!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user