Initial setup: Flutter blockchain explorer app
- Clean architecture with feature-based organization - Riverpod state management, Dio HTTP, GoRouter navigation - Dashboard, blocks, wallet, transactions, mining screens - Dark crypto theme with JetBrains Mono font
This commit is contained in:
commit
5a8300c5ea
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
*.class
|
||||||
|
*.lock
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
.idea/
|
||||||
|
.vagrant/
|
||||||
|
.sconsign.dblite
|
||||||
|
.svn/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.env
|
||||||
100
README.md
Normal file
100
README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# blockchain-flutter
|
||||||
|
|
||||||
|
Flutter app for blockchain explorer, wallet management, and mining. Connects to the blockchain node via REST API.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Clean Architecture with feature-based organization:
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── main.dart
|
||||||
|
├── core/
|
||||||
|
│ ├── network/
|
||||||
|
│ │ └── api_client.dart # Dio HTTP client setup
|
||||||
|
│ ├── theme/
|
||||||
|
│ │ └── app_theme.dart # Dark theme, crypto aesthetic
|
||||||
|
│ └── utils/
|
||||||
|
│ └── formatters.dart # Amount, hash, date formatting
|
||||||
|
├── features/
|
||||||
|
│ ├── dashboard/ # Home: chain stats, recent blocks
|
||||||
|
│ │ ├── dashboard_screen.dart
|
||||||
|
│ │ └── dashboard_provider.dart
|
||||||
|
│ ├── blocks/ # Block explorer (list + detail)
|
||||||
|
│ │ ├── blocks_screen.dart
|
||||||
|
│ │ ├── block_detail_screen.dart
|
||||||
|
│ │ └── blocks_provider.dart
|
||||||
|
│ ├── wallet/ # Create, view balance, list wallets
|
||||||
|
│ │ ├── wallet_screen.dart
|
||||||
|
│ │ └── wallet_provider.dart
|
||||||
|
│ ├── transactions/ # Send tx, pending list
|
||||||
|
│ │ ├── transactions_screen.dart
|
||||||
|
│ │ ├── send_tx_screen.dart
|
||||||
|
│ │ └── transactions_provider.dart
|
||||||
|
│ └── mining/ # Mine button, difficulty info
|
||||||
|
│ ├── mining_screen.dart
|
||||||
|
│ └── mining_provider.dart
|
||||||
|
└── routing/
|
||||||
|
└── app_router.dart # GoRouter navigation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Component | Choice | Rationale |
|
||||||
|
|-----------|--------|-----------|
|
||||||
|
| State management | Riverpod | Less boilerplate than BLoC, familiar |
|
||||||
|
| HTTP client | Dio | Interceptors, retry support |
|
||||||
|
| Navigation | GoRouter | Declarative routing |
|
||||||
|
| Models | Freezed + json_serializable | Immutable data classes |
|
||||||
|
| Secure storage | flutter_secure_storage | Wallet private keys |
|
||||||
|
| Charts | fl_chart | Chain visualization |
|
||||||
|
| Real-time | Polling (5-10s) | Simple, no WebSocket needed |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Dashboard**: chain height, difficulty, block reward, recent blocks
|
||||||
|
- **Block Explorer**: browse blocks, view transactions per block
|
||||||
|
- **Wallet**: generate keypairs, check balances, manage multiple wallets
|
||||||
|
- **Transactions**: send signed transactions, view pending pool
|
||||||
|
- **Mining**: one-tap mining, difficulty display, reward info
|
||||||
|
|
||||||
|
## Screens
|
||||||
|
|
||||||
|
1. **Dashboard** - overview with chain stats and recent activity
|
||||||
|
2. **Blocks** - paginated block list with expandable details
|
||||||
|
3. **Block Detail** - full block info with transaction list
|
||||||
|
4. **Wallets** - wallet list with balances, create new
|
||||||
|
5. **Send Transaction** - form: from, to, amount, sign & submit
|
||||||
|
6. **Pending Transactions** - current mempool
|
||||||
|
7. **Mining** - mine button with result display
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Generate code (Freezed models)
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Run (blockchain-node must be running on localhost:3000)
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Run on web
|
||||||
|
flutter run -d chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Node URL is configured in `lib/core/network/api_client.dart`. Defaults to `http://localhost:3000`.
|
||||||
|
|
||||||
|
For Android emulator, use `http://10.0.2.2:3000` to reach host localhost.
|
||||||
|
|
||||||
|
## Related Repos
|
||||||
|
|
||||||
|
- [blockchain-core](../blockchain-core) - Core library + REST API node
|
||||||
|
- [blockchain-cli](../blockchain-cli) - Rust CLI tool
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
105
lib/core/network/api_client.dart
Normal file
105
lib/core/network/api_client.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
static const String _defaultBaseUrl = 'http://localhost:3000';
|
||||||
|
// Use 10.0.2.2 for Android emulator to reach host localhost
|
||||||
|
static const String _androidEmulatorUrl = 'http://10.0.2.2:3000';
|
||||||
|
|
||||||
|
late final Dio dio;
|
||||||
|
|
||||||
|
ApiClient({String? baseUrl}) {
|
||||||
|
final url = baseUrl ?? _resolveBaseUrl();
|
||||||
|
|
||||||
|
dio = Dio(BaseOptions(
|
||||||
|
baseUrl: url,
|
||||||
|
connectTimeout: const Duration(seconds: 5),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
));
|
||||||
|
|
||||||
|
dio.interceptors.add(LogInterceptor(
|
||||||
|
requestBody: true,
|
||||||
|
responseBody: true,
|
||||||
|
logPrint: (log) => debugPrint(log.toString()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _resolveBaseUrl() {
|
||||||
|
// Android emulator uses 10.0.2.2 to reach host machine
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
return _androidEmulatorUrl;
|
||||||
|
}
|
||||||
|
return _defaultBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain
|
||||||
|
Future<Map<String, dynamic>> getChainInfo() async {
|
||||||
|
final response = await dio.get('/api/chain/info');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> validateChain() async {
|
||||||
|
final response = await dio.post('/api/chain/validate');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks
|
||||||
|
Future<Map<String, dynamic>> getBlocks({int offset = 0, int limit = 10}) async {
|
||||||
|
final response = await dio.get('/api/blocks', queryParameters: {
|
||||||
|
'offset': offset,
|
||||||
|
'limit': limit,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getBlock(String hash) async {
|
||||||
|
final response = await dio.get('/api/blocks/$hash');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallets
|
||||||
|
Future<Map<String, dynamic>> createWallet() async {
|
||||||
|
final response = await dio.post('/api/wallets');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getBalance(String address) async {
|
||||||
|
final response = await dio.get('/api/wallets/$address/balance');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
Future<Map<String, dynamic>> submitTransaction(Map<String, dynamic> tx) async {
|
||||||
|
final response = await dio.post('/api/transactions', data: tx);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> getPendingTransactions() async {
|
||||||
|
final response = await dio.get('/api/transactions/pending');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mining
|
||||||
|
Future<Map<String, dynamic>> mine(String minerAddress) async {
|
||||||
|
final response = await dio.post('/api/mine', data: {
|
||||||
|
'miner_address': minerAddress,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getMiningStatus() async {
|
||||||
|
final response = await dio.get('/api/mining/status');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health
|
||||||
|
Future<bool> healthCheck() async {
|
||||||
|
try {
|
||||||
|
await dio.get('/api/health');
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/core/theme/app_theme.dart
Normal file
49
lib/core/theme/app_theme.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
static const _primaryColor = Color(0xFF6C63FF);
|
||||||
|
static const _accentColor = Color(0xFF00D9FF);
|
||||||
|
static const _backgroundColor = Color(0xFF0D1117);
|
||||||
|
static const _surfaceColor = Color(0xFF161B22);
|
||||||
|
static const _cardColor = Color(0xFF21262D);
|
||||||
|
|
||||||
|
static ThemeData get darkTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: const ColorScheme.dark(
|
||||||
|
primary: _primaryColor,
|
||||||
|
secondary: _accentColor,
|
||||||
|
surface: _surfaceColor,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: _backgroundColor,
|
||||||
|
cardColor: _cardColor,
|
||||||
|
textTheme: GoogleFonts.jetBrainsMonoTextTheme(
|
||||||
|
ThemeData.dark().textTheme,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: _surfaceColor,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: _primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cardTheme: CardTheme(
|
||||||
|
color: _cardColor,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: Colors.white.withAlpha(25)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/core/utils/formatters.dart
Normal file
31
lib/core/utils/formatters.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class Formatters {
|
||||||
|
/// Format a hash for display (first 8 + ... + last 8).
|
||||||
|
static String truncateHash(String hash, {int chars = 8}) {
|
||||||
|
if (hash.length <= chars * 2) return hash;
|
||||||
|
return '${hash.substring(0, chars)}...${hash.substring(hash.length - chars)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format amount from smallest unit to display unit.
|
||||||
|
/// 50_000_000 -> "50.000000"
|
||||||
|
static String formatAmount(int amount, {int decimals = 6}) {
|
||||||
|
final value = amount / 1000000;
|
||||||
|
return value.toStringAsFixed(decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a timestamp string to readable date.
|
||||||
|
static String formatTimestamp(String timestamp) {
|
||||||
|
try {
|
||||||
|
final dt = DateTime.parse(timestamp);
|
||||||
|
return DateFormat('yyyy-MM-dd HH:mm:ss').format(dt.toLocal());
|
||||||
|
} catch (_) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format large numbers with comma separators.
|
||||||
|
static String formatNumber(int number) {
|
||||||
|
return NumberFormat('#,###').format(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/features/blocks/block_detail_screen.dart
Normal file
21
lib/features/blocks/block_detail_screen.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class BlockDetailScreen extends ConsumerWidget {
|
||||||
|
final String hash;
|
||||||
|
|
||||||
|
const BlockDetailScreen({super.key, required this.hash});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Block Detail')),
|
||||||
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
'Block Detail\n\nHash: $hash',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/blocks/blocks_screen.dart
Normal file
19
lib/features/blocks/blocks_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class BlocksScreen extends ConsumerWidget {
|
||||||
|
const BlocksScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Block Explorer')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Block Explorer\n\nBrowse blocks with pagination.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/dashboard/dashboard_screen.dart
Normal file
19
lib/features/dashboard/dashboard_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class DashboardScreen extends ConsumerWidget {
|
||||||
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Dashboard')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Blockchain Dashboard\n\nChain stats, recent blocks, and network overview.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/mining/mining_screen.dart
Normal file
19
lib/features/mining/mining_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class MiningScreen extends ConsumerWidget {
|
||||||
|
const MiningScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Mining')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Mining\n\nMine blocks, view difficulty and rewards.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/transactions/send_tx_screen.dart
Normal file
19
lib/features/transactions/send_tx_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class SendTxScreen extends ConsumerWidget {
|
||||||
|
const SendTxScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Send Transaction')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Send Transaction\n\nForm: from, to, amount.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/transactions/transactions_screen.dart
Normal file
19
lib/features/transactions/transactions_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class TransactionsScreen extends ConsumerWidget {
|
||||||
|
const TransactionsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Transactions')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Pending Transactions\n\nView mempool.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/wallet/wallet_screen.dart
Normal file
19
lib/features/wallet/wallet_screen.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class WalletScreen extends ConsumerWidget {
|
||||||
|
const WalletScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Wallets')),
|
||||||
|
body: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Wallet Management\n\nCreate wallets, view balances.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/main.dart
Normal file
25
lib/main.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'routing/app_router.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const ProviderScope(child: BlockchainApp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockchainApp extends ConsumerWidget {
|
||||||
|
const BlockchainApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'Blockchain Explorer',
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
routerConfig: router,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/routing/app_router.dart
Normal file
95
lib/routing/app_router.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../features/dashboard/dashboard_screen.dart';
|
||||||
|
import '../features/blocks/blocks_screen.dart';
|
||||||
|
import '../features/blocks/block_detail_screen.dart';
|
||||||
|
import '../features/wallet/wallet_screen.dart';
|
||||||
|
import '../features/transactions/transactions_screen.dart';
|
||||||
|
import '../features/transactions/send_tx_screen.dart';
|
||||||
|
import '../features/mining/mining_screen.dart';
|
||||||
|
|
||||||
|
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
|
return GoRouter(
|
||||||
|
initialLocation: '/',
|
||||||
|
routes: [
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => ScaffoldWithNav(child: child),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (context, state) => const DashboardScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/blocks',
|
||||||
|
builder: (context, state) => const BlocksScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/blocks/:hash',
|
||||||
|
builder: (context, state) => BlockDetailScreen(
|
||||||
|
hash: state.pathParameters['hash']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/wallets',
|
||||||
|
builder: (context, state) => const WalletScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/transactions',
|
||||||
|
builder: (context, state) => const TransactionsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/transactions/send',
|
||||||
|
builder: (context, state) => const SendTxScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/mining',
|
||||||
|
builder: (context, state) => const MiningScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
class ScaffoldWithNav extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
const ScaffoldWithNav({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: child,
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
selectedIndex: _calculateIndex(GoRouterState.of(context).uri.path),
|
||||||
|
onDestinationSelected: (index) => _onTap(context, index),
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(icon: Icon(Icons.dashboard), label: 'Dashboard'),
|
||||||
|
NavigationDestination(icon: Icon(Icons.view_in_ar), label: 'Blocks'),
|
||||||
|
NavigationDestination(icon: Icon(Icons.account_balance_wallet), label: 'Wallets'),
|
||||||
|
NavigationDestination(icon: Icon(Icons.swap_horiz), label: 'Transactions'),
|
||||||
|
NavigationDestination(icon: Icon(Icons.hardware), label: 'Mining'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _calculateIndex(String path) {
|
||||||
|
if (path.startsWith('/blocks')) return 1;
|
||||||
|
if (path.startsWith('/wallets')) return 2;
|
||||||
|
if (path.startsWith('/transactions')) return 3;
|
||||||
|
if (path.startsWith('/mining')) return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0: context.go('/');
|
||||||
|
case 1: context.go('/blocks');
|
||||||
|
case 2: context.go('/wallets');
|
||||||
|
case 3: context.go('/transactions');
|
||||||
|
case 4: context.go('/mining');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
pubspec.yaml
Normal file
34
pubspec.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: blockchain_flutter
|
||||||
|
description: Flutter app for blockchain explorer, wallet management, and mining.
|
||||||
|
publish_to: 'none'
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.6.0
|
||||||
|
flutter: ">=3.27.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_riverpod: ^2.6.1
|
||||||
|
riverpod_annotation: ^2.6.1
|
||||||
|
dio: ^5.7.0
|
||||||
|
go_router: ^14.8.1
|
||||||
|
freezed_annotation: ^2.4.4
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
flutter_secure_storage: ^9.2.4
|
||||||
|
fl_chart: ^0.70.2
|
||||||
|
intl: ^0.19.0
|
||||||
|
google_fonts: ^6.2.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^5.0.0
|
||||||
|
build_runner: ^2.4.14
|
||||||
|
freezed: ^2.5.7
|
||||||
|
json_serializable: ^6.9.4
|
||||||
|
riverpod_generator: ^2.6.3
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
Loading…
Reference in New Issue
Block a user