Files
peyawallet/lib/ui/screens/mining_screen.dart
Codex Bot eef05a265d
build / Build Linux (lite) (push) Successful in 2m54s
build / Build Linux (mining) (push) Successful in 2m22s
build / Build Windows (mining) (push) Has been cancelled
build / Build Windows (lite) (push) Has been cancelled
Log xmrig startup failures in peyawallet
2026-04-20 23:46:09 +02:00

615 lines
21 KiB
Dart

import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../domain/models.dart';
import '../../state/mining_controller.dart';
import '../../state/providers.dart';
import '../l10n/app_localizations_ext.dart';
class MiningScreen extends ConsumerStatefulWidget {
const MiningScreen({super.key});
@override
ConsumerState<MiningScreen> createState() => _MiningScreenState();
}
class _MiningScreenState extends ConsumerState<MiningScreen> {
late MiningMode _mode;
late int _cpuThreads;
late final TextEditingController _apiPortController;
bool _dirty = false;
@override
void initState() {
super.initState();
final config = ref.read(appConfigControllerProvider).miningConfig;
_mode = config.mode;
_cpuThreads = config.cpuThreads;
_apiPortController = TextEditingController(text: config.apiPort.toString());
}
@override
void dispose() {
_apiPortController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final config = ref.watch(appConfigControllerProvider);
final walletState = ref.watch(walletControllerProvider);
final miningState = ref.watch(miningControllerProvider);
final maxThreads = math.max(1, math.min(Platform.numberOfProcessors, 64));
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.miningTitle,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
l10n.miningScreenSubtitle,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
_SectionCard(
title: l10n.miningControlsTitle,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton.icon(
onPressed: miningState.busy || miningState.running
? null
: _startMining,
icon: const Icon(Symbols.play_arrow),
label: Text(l10n.miningStartAction),
),
OutlinedButton.icon(
onPressed: miningState.busy || !miningState.running
? null
: _stopMining,
icon: const Icon(Symbols.stop),
label: Text(l10n.miningStopAction),
),
OutlinedButton.icon(
onPressed: miningState.busy || !miningState.running
? null
: _restartMining,
icon: const Icon(Symbols.refresh),
label: Text(l10n.localNodeRestartAction),
),
OutlinedButton.icon(
onPressed: () => _showSettingsDialog(maxThreads),
icon: const Icon(Symbols.settings),
label: Text(l10n.miningSettingsAction),
),
],
),
const SizedBox(height: 12),
Text(
miningState.error ??
(miningState.running
? l10n.miningStatusRunning
: l10n.miningStatusStopped),
style: theme.textTheme.bodyMedium?.copyWith(
color: miningState.error == null
? theme.colorScheme.onSurfaceVariant
: theme.colorScheme.error,
),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: l10n.miningCurrentConfigTitle,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoTile(
label: l10n.miningWalletAddressLabel,
value: walletState.walletInfo?.address.isNotEmpty == true
? walletState.walletInfo!.address
: l10n.miningNoWalletAddress,
icon: Symbols.account_balance_wallet,
),
const SizedBox(height: 12),
_InfoTile(
label: l10n.miningModeLabel,
value: config.miningConfig.mode == MiningMode.solo
? l10n.miningModeSolo
: l10n.miningModePool,
icon: config.miningConfig.mode == MiningMode.solo
? Symbols.lan
: Symbols.hub,
),
const SizedBox(height: 12),
_InfoTile(
label: l10n.miningTargetLabel,
value: config.miningConfig.mode == MiningMode.solo
? l10n.miningSoloTargetValue
: '${config.miningConfig.poolHost}:${config.miningConfig.poolPort}',
icon: Symbols.route,
),
const SizedBox(height: 12),
_InfoTile(
label: l10n.miningThreadsLabel,
value: config.miningConfig.cpuThreads.toString(),
icon: Symbols.memory,
),
const SizedBox(height: 12),
_InfoTile(
label: l10n.miningApiPortLabel,
value: config.miningConfig.apiPort.toString(),
icon: Symbols.settings_ethernet,
),
const SizedBox(height: 12),
_InfoTile(
label: l10n.miningCurrentDiffLabel,
value: miningState.summary?.currentDifficulty.toString() ??
l10n.miningDataUnknownValue,
icon: Symbols.speed,
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: l10n.miningStatsTitle,
child: Wrap(
spacing: 12,
runSpacing: 12,
children: [
_StatTile(
label: l10n.transactionStatusLabel,
value: miningState.running
? l10n.miningStatusRunning
: l10n.miningStatusStopped,
),
_StatTile(
label: l10n.miningHashrate10sLabel,
value: _formatHashrate(miningState.summary?.hashrate10s, l10n),
),
_StatTile(
label: l10n.miningHashrate1mLabel,
value: _formatHashrate(miningState.summary?.hashrate1m, l10n),
),
_StatTile(
label: l10n.miningLabelHashrate15m,
value: _formatHashrate(miningState.summary?.hashrate15m, l10n),
),
_StatTile(
label: l10n.miningJobsLabel,
value: l10n.miningDataUnknownValue,
),
_StatTile(
label: l10n.miningAcceptedSharesLabel,
value: miningState.summary?.acceptedShares.toString() ??
l10n.miningDataUnknownValue,
),
_StatTile(
label: l10n.miningRejectedSharesLabel,
value: miningState.summary?.rejectedShares.toString() ??
l10n.miningDataUnknownValue,
),
_StatTile(
label: l10n.miningLabelUptime,
value: _formatUptime(
miningState.summary?.uptimeSeconds,
l10n,
),
),
],
),
),
],
),
),
),
);
}
void _resetDraft() {
final config = ref.read(appConfigControllerProvider).miningConfig;
setState(() {
_mode = config.mode;
_cpuThreads = config.cpuThreads;
_apiPortController.text = config.apiPort.toString();
_dirty = false;
});
}
Future<void> _saveConfig() async {
final l10n = context.l10n;
final apiPort = int.tryParse(_apiPortController.text.trim());
if (apiPort == null || apiPort <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.miningConfigInvalid)),
);
return;
}
final next = MiningConfig(
mode: _mode,
cpuThreads: _cpuThreads,
poolHost: 'peya.cryptohash.top',
poolPort: 3333,
apiPort: apiPort,
);
await ref.read(appConfigControllerProvider.notifier).setMiningConfig(next);
if (!mounted) {
return;
}
setState(() => _dirty = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.miningSettingsSaved)),
);
await ref.read(miningControllerProvider.notifier).refresh();
}
Future<void> _showSettingsDialog(int maxThreads) async {
final l10n = context.l10n;
final theme = Theme.of(context);
await showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: theme.colorScheme.surface,
title: Text(l10n.miningSettingsAction),
content: SizedBox(
width: 520,
child: StatefulBuilder(
builder: (context, setDialogState) {
final nodeMode = ref.read(appConfigControllerProvider).nodeConfig.mode;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<MiningMode>(
value: _mode,
dropdownColor: theme.colorScheme.surface,
decoration: InputDecoration(
labelText: l10n.miningModeLabel,
),
items: [
DropdownMenuItem(
value: MiningMode.solo,
child: Text(l10n.miningModeSolo),
),
DropdownMenuItem(
value: MiningMode.pool,
child: Text(l10n.miningModePool),
),
],
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
_mode = value;
_dirty = true;
});
setDialogState(() {});
},
),
const SizedBox(height: 16),
if (_mode == MiningMode.solo) ...[
_InfoTile(
label: l10n.miningTargetLabel,
value: l10n.miningSoloTargetValue,
icon: Symbols.lan,
),
const SizedBox(height: 12),
Text(
nodeMode == NodeMode.local
? l10n.miningSoloHint
: l10n.miningSoloNeedsLocalNode,
style: theme.textTheme.bodyMedium?.copyWith(
color: nodeMode == NodeMode.local
? theme.colorScheme.onSurfaceVariant
: theme.colorScheme.error,
),
),
] else ...[
_InfoTile(
label: l10n.miningTargetLabel,
value: 'peya.cryptohash.top:3333',
icon: Symbols.hub,
),
const SizedBox(height: 12),
Text(
l10n.miningPoolFixedHint,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 20),
Text(
'${l10n.miningThreadsLabel}: $_cpuThreads',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Slider(
value: _cpuThreads.toDouble(),
min: 1,
max: maxThreads.toDouble(),
divisions: math.max(0, maxThreads - 1),
label: _cpuThreads.toString(),
onChanged: (value) {
final next = value.round();
setState(() {
_cpuThreads = next;
_dirty = true;
});
setDialogState(() {});
},
),
const SizedBox(height: 8),
TextFormField(
controller: _apiPortController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: l10n.miningApiPortLabel,
),
onChanged: (_) {
setState(() => _dirty = true);
setDialogState(() {});
},
),
],
);
},
),
),
actions: [
TextButton(
onPressed: () {
_resetDraft();
Navigator.of(dialogContext).pop();
},
child: Text(l10n.miningResetDraftAction),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l10n.cancelAction),
),
FilledButton(
onPressed: () {
final apiPort = int.tryParse(_apiPortController.text.trim());
return apiPort != null && apiPort > 0;
}()
? () async {
await _saveConfig();
if (mounted) {
Navigator.of(dialogContext).pop();
}
}
: null,
child: Text(l10n.saveAction),
),
],
);
},
);
}
Future<void> _startMining() async {
final l10n = context.l10n;
final started = await ref.read(miningControllerProvider.notifier).start();
final miningState = ref.read(miningControllerProvider);
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
started
? l10n.miningMinerStartSuccess
: (miningState.error?.isNotEmpty == true
? '${l10n.miningMinerStartFailure}: ${miningState.error}'
: l10n.miningMinerStartFailure),
),
),
);
}
Future<void> _stopMining() async {
final l10n = context.l10n;
final stopped = await ref.read(miningControllerProvider.notifier).stop();
final miningState = ref.read(miningControllerProvider);
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
stopped
? l10n.miningMinerStopSuccess
: (miningState.error?.isNotEmpty == true
? '${l10n.miningMinerStopFailure}: ${miningState.error}'
: l10n.miningMinerStopFailure),
),
),
);
}
Future<void> _restartMining() async {
final l10n = context.l10n;
final restarted = await ref.read(miningControllerProvider.notifier).restart();
final miningState = ref.read(miningControllerProvider);
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
restarted
? l10n.miningMinerRestartSuccess
: (miningState.error?.isNotEmpty == true
? '${l10n.miningMinerRestartFailure}: ${miningState.error}'
: l10n.miningMinerRestartFailure),
),
),
);
}
String _formatHashrate(double? value, dynamic l10n) {
if (value == null) {
return l10n.miningDataUnknownValue;
}
return '${value.toStringAsFixed(1)} H/s';
}
String _formatUptime(int? seconds, dynamic l10n) {
if (seconds == null) {
return l10n.miningDataUnknownValue;
}
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
final secs = seconds % 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
if (minutes > 0) {
return '${minutes}m ${secs}s';
}
return '${secs}s';
}
}
class _SectionCard extends StatelessWidget {
const _SectionCard({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: double.infinity,
child: Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 16),
child,
],
),
),
),
);
}
}
class _StatTile extends StatelessWidget {
const _StatTile({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: 180,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
value,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _InfoTile extends StatelessWidget {
const _InfoTile({
required this.label,
required this.value,
required this.icon,
});
final String label;
final String value;
final IconData icon;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
SelectableText(
value,
style: theme.textTheme.bodyLarge,
),
],
),
),
],
);
}
}