Add xmrig service layer to peyawallet
This commit is contained in:
@@ -362,11 +362,20 @@
|
||||
"miningControlsTitle": "Controls",
|
||||
"miningBackendPending": "Miner process control will be enabled in the next step. This screen already stores the target config and release layout.",
|
||||
"miningStatsTitle": "Stats",
|
||||
"miningStatusRunning": "Miner is running.",
|
||||
"miningStatusStopped": "Miner is stopped.",
|
||||
"miningHashrate10sLabel": "Hashrate (10s)",
|
||||
"miningHashrate1mLabel": "Hashrate (1m)",
|
||||
"miningJobsLabel": "Jobs",
|
||||
"miningAcceptedSharesLabel": "Accepted shares",
|
||||
"miningRejectedSharesLabel": "Rejected shares",
|
||||
"miningCurrentDiffLabel": "Current difficulty",
|
||||
"miningMinerStartSuccess": "Miner started",
|
||||
"miningMinerStartFailure": "Failed to start miner",
|
||||
"miningMinerStopSuccess": "Miner stopped",
|
||||
"miningMinerStopFailure": "Failed to stop miner",
|
||||
"miningMinerRestartSuccess": "Miner restarted",
|
||||
"miningMinerRestartFailure": "Failed to restart miner",
|
||||
"miningBundledTitle": "Bundled miner",
|
||||
"miningBundledEnabled": "This build includes the bundled XMRig binary.",
|
||||
"miningBundledDisabled": "This release flavor does not include the bundled miner.",
|
||||
|
||||
@@ -1796,6 +1796,18 @@ abstract class AppLocalizations {
|
||||
/// **'Stats'**
|
||||
String get miningStatsTitle;
|
||||
|
||||
/// No description provided for @miningStatusRunning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Miner is running.'**
|
||||
String get miningStatusRunning;
|
||||
|
||||
/// No description provided for @miningStatusStopped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Miner is stopped.'**
|
||||
String get miningStatusStopped;
|
||||
|
||||
/// No description provided for @miningHashrate10sLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1826,6 +1838,48 @@ abstract class AppLocalizations {
|
||||
/// **'Rejected shares'**
|
||||
String get miningRejectedSharesLabel;
|
||||
|
||||
/// No description provided for @miningCurrentDiffLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current difficulty'**
|
||||
String get miningCurrentDiffLabel;
|
||||
|
||||
/// No description provided for @miningMinerStartSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Miner started'**
|
||||
String get miningMinerStartSuccess;
|
||||
|
||||
/// No description provided for @miningMinerStartFailure.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to start miner'**
|
||||
String get miningMinerStartFailure;
|
||||
|
||||
/// No description provided for @miningMinerStopSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Miner stopped'**
|
||||
String get miningMinerStopSuccess;
|
||||
|
||||
/// No description provided for @miningMinerStopFailure.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to stop miner'**
|
||||
String get miningMinerStopFailure;
|
||||
|
||||
/// No description provided for @miningMinerRestartSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Miner restarted'**
|
||||
String get miningMinerRestartSuccess;
|
||||
|
||||
/// No description provided for @miningMinerRestartFailure.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to restart miner'**
|
||||
String get miningMinerRestartFailure;
|
||||
|
||||
/// No description provided for @miningBundledTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -907,6 +907,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get miningStatsTitle => 'Stats';
|
||||
|
||||
@override
|
||||
String get miningStatusRunning => 'Miner is running.';
|
||||
|
||||
@override
|
||||
String get miningStatusStopped => 'Miner is stopped.';
|
||||
|
||||
@override
|
||||
String get miningHashrate10sLabel => 'Hashrate (10s)';
|
||||
|
||||
@@ -922,6 +928,27 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get miningRejectedSharesLabel => 'Rejected shares';
|
||||
|
||||
@override
|
||||
String get miningCurrentDiffLabel => 'Current difficulty';
|
||||
|
||||
@override
|
||||
String get miningMinerStartSuccess => 'Miner started';
|
||||
|
||||
@override
|
||||
String get miningMinerStartFailure => 'Failed to start miner';
|
||||
|
||||
@override
|
||||
String get miningMinerStopSuccess => 'Miner stopped';
|
||||
|
||||
@override
|
||||
String get miningMinerStopFailure => 'Failed to stop miner';
|
||||
|
||||
@override
|
||||
String get miningMinerRestartSuccess => 'Miner restarted';
|
||||
|
||||
@override
|
||||
String get miningMinerRestartFailure => 'Failed to restart miner';
|
||||
|
||||
@override
|
||||
String get miningBundledTitle => 'Bundled miner';
|
||||
|
||||
|
||||
@@ -911,6 +911,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get miningStatsTitle => 'Statystyki';
|
||||
|
||||
@override
|
||||
String get miningStatusRunning => 'Miner działa.';
|
||||
|
||||
@override
|
||||
String get miningStatusStopped => 'Miner jest zatrzymany.';
|
||||
|
||||
@override
|
||||
String get miningHashrate10sLabel => 'Hashrate (10s)';
|
||||
|
||||
@@ -926,6 +932,27 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get miningRejectedSharesLabel => 'Odrzucone udziały';
|
||||
|
||||
@override
|
||||
String get miningCurrentDiffLabel => 'Aktualna trudność';
|
||||
|
||||
@override
|
||||
String get miningMinerStartSuccess => 'Miner uruchomiony';
|
||||
|
||||
@override
|
||||
String get miningMinerStartFailure => 'Nie udało się uruchomić minera';
|
||||
|
||||
@override
|
||||
String get miningMinerStopSuccess => 'Miner zatrzymany';
|
||||
|
||||
@override
|
||||
String get miningMinerStopFailure => 'Nie udało się zatrzymać minera';
|
||||
|
||||
@override
|
||||
String get miningMinerRestartSuccess => 'Miner zrestartowany';
|
||||
|
||||
@override
|
||||
String get miningMinerRestartFailure => 'Nie udało się zrestartować minera';
|
||||
|
||||
@override
|
||||
String get miningBundledTitle => 'Bundlowany miner';
|
||||
|
||||
|
||||
@@ -362,11 +362,20 @@
|
||||
"miningControlsTitle": "Sterowanie",
|
||||
"miningBackendPending": "Sterowanie procesem minera zostanie dopięte w następnym kroku. Ten ekran zapisuje już docelową konfigurację i układ releasu.",
|
||||
"miningStatsTitle": "Statystyki",
|
||||
"miningStatusRunning": "Miner działa.",
|
||||
"miningStatusStopped": "Miner jest zatrzymany.",
|
||||
"miningHashrate10sLabel": "Hashrate (10s)",
|
||||
"miningHashrate1mLabel": "Hashrate (1m)",
|
||||
"miningJobsLabel": "Joby",
|
||||
"miningAcceptedSharesLabel": "Zaakceptowane udziały",
|
||||
"miningRejectedSharesLabel": "Odrzucone udziały",
|
||||
"miningCurrentDiffLabel": "Aktualna trudność",
|
||||
"miningMinerStartSuccess": "Miner uruchomiony",
|
||||
"miningMinerStartFailure": "Nie udało się uruchomić minera",
|
||||
"miningMinerStopSuccess": "Miner zatrzymany",
|
||||
"miningMinerStopFailure": "Nie udało się zatrzymać minera",
|
||||
"miningMinerRestartSuccess": "Miner zrestartowany",
|
||||
"miningMinerRestartFailure": "Nie udało się zrestartować minera",
|
||||
"miningBundledTitle": "Bundlowany miner",
|
||||
"miningBundledEnabled": "Ten build zawiera bundlowaną binarkę XMRig.",
|
||||
"miningBundledDisabled": "Ten wariant releasu nie zawiera bundlowanego minera.",
|
||||
|
||||
@@ -55,6 +55,18 @@ class AppPaths {
|
||||
return File(p.join((await appSupportDir()).path, 'peyad-launcher.log'));
|
||||
}
|
||||
|
||||
static Future<File> minerPidFile() async {
|
||||
return File(p.join((await appSupportDir()).path, 'xmrig.pid'));
|
||||
}
|
||||
|
||||
static Future<File> minerLogFile() async {
|
||||
return File(p.join((await appSupportDir()).path, 'xmrig.log'));
|
||||
}
|
||||
|
||||
static Future<File> minerLauncherLogFile() async {
|
||||
return File(p.join((await appSupportDir()).path, 'xmrig-launcher.log'));
|
||||
}
|
||||
|
||||
static Future<List<File>> legacyConfigFiles() async {
|
||||
final targetRoot = Directory(_targetAppSupportPath());
|
||||
final legacyRoot = Directory(_legacyAppSupportPath());
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'app_paths.dart';
|
||||
import 'mining_service.dart';
|
||||
|
||||
class BundledMiningService implements MiningService {
|
||||
BundledMiningService({required Logger logger}) : _logger = logger;
|
||||
|
||||
final Logger _logger;
|
||||
Future<bool>? _startFuture;
|
||||
MinerLaunchConfig? _lastConfig;
|
||||
|
||||
@override
|
||||
Future<bool> isRunning({MinerLaunchConfig? config}) async {
|
||||
final resolved = config ?? _lastConfig;
|
||||
if (resolved == null) {
|
||||
return false;
|
||||
}
|
||||
if (await _isApiAvailable(resolved)) {
|
||||
return true;
|
||||
}
|
||||
final pid = await _readPid();
|
||||
if (pid == null) {
|
||||
return false;
|
||||
}
|
||||
return _isProcessAlive(pid);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> start({required MinerLaunchConfig config}) async {
|
||||
if (_startFuture != null) {
|
||||
return _startFuture!;
|
||||
}
|
||||
_startFuture = _startInternal(config);
|
||||
try {
|
||||
return await _startFuture!;
|
||||
} finally {
|
||||
_startFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stop({MinerLaunchConfig? config}) async {
|
||||
final resolved = config ?? _lastConfig;
|
||||
final pidFile = await AppPaths.minerPidFile();
|
||||
final pid = await _readPid();
|
||||
if (pid == null) {
|
||||
if (resolved == null) {
|
||||
return true;
|
||||
}
|
||||
return !(await _isApiAvailable(resolved));
|
||||
}
|
||||
|
||||
var stopped = Process.killPid(pid);
|
||||
if (!stopped) {
|
||||
_logger.w('Failed to signal xmrig process ($pid)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await _waitForExit(pid, resolved)) {
|
||||
try {
|
||||
Process.killPid(pid, ProcessSignal.sigkill);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final exited = await _waitForExit(pid, resolved);
|
||||
if (exited && await pidFile.exists()) {
|
||||
try {
|
||||
await pidFile.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
return exited;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> restart({required MinerLaunchConfig config}) async {
|
||||
final stopped = await stop(config: config);
|
||||
if (!stopped) {
|
||||
return false;
|
||||
}
|
||||
return start(config: config);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<XmrigSummary?> fetchSummary({required MinerLaunchConfig config}) async {
|
||||
HttpClient? client;
|
||||
try {
|
||||
client = HttpClient();
|
||||
final uri = Uri.parse('http://127.0.0.1:${config.apiPort}/1/summary');
|
||||
final request = await client.getUrl(uri);
|
||||
final response = await request.close();
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
return null;
|
||||
}
|
||||
final body = await utf8.decoder.bind(response).join();
|
||||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||||
final hashrate = json['hashrate'] as Map<String, dynamic>? ?? const {};
|
||||
final total = hashrate['total'] as List<dynamic>? ?? const [];
|
||||
final results = json['results'] as Map<String, dynamic>? ?? const {};
|
||||
final connection =
|
||||
json['connection'] as Map<String, dynamic>? ?? const {};
|
||||
final sharesGood = (results['shares_good'] as num?)?.toInt() ?? 0;
|
||||
final sharesTotal = (results['shares_total'] as num?)?.toInt() ?? 0;
|
||||
final rejected = sharesTotal - sharesGood;
|
||||
return XmrigSummary(
|
||||
status: 'running',
|
||||
pool: connection['pool'] as String? ?? '',
|
||||
hashrate10s: _readHashrate(total, 0),
|
||||
hashrate1m: _readHashrate(total, 1),
|
||||
hashrate15m: _readHashrate(total, 2),
|
||||
acceptedShares: sharesGood,
|
||||
totalShares: sharesTotal,
|
||||
rejectedShares: rejected < 0 ? 0 : rejected,
|
||||
uptimeSeconds: (connection['uptime'] as num?)?.toInt() ?? 0,
|
||||
currentDifficulty: (results['diff_current'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
} catch (error) {
|
||||
_logger.w('Failed to read xmrig summary: $error');
|
||||
return null;
|
||||
} finally {
|
||||
client?.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _startInternal(MinerLaunchConfig config) async {
|
||||
_lastConfig = config;
|
||||
if (await _isApiAvailable(config)) {
|
||||
return true;
|
||||
}
|
||||
final binary = await _locateBinary();
|
||||
if (binary == null) {
|
||||
_logger.w('Bundled xmrig binary not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
final pidFile = await AppPaths.minerPidFile();
|
||||
final logFile = await AppPaths.minerLogFile();
|
||||
final launcherLogFile = await AppPaths.minerLauncherLogFile();
|
||||
await launcherLogFile.parent.create(recursive: true);
|
||||
|
||||
final args = <String>[
|
||||
'--coin',
|
||||
'PEY',
|
||||
'-a',
|
||||
'rx/0',
|
||||
'-u',
|
||||
config.walletAddress,
|
||||
'-p',
|
||||
'x',
|
||||
'-t',
|
||||
config.cpuThreads.clamp(1, 64).toString(),
|
||||
'--http-host',
|
||||
'127.0.0.1',
|
||||
'--http-port',
|
||||
config.apiPort.toString(),
|
||||
'--log-file',
|
||||
logFile.path,
|
||||
'--print-time=60',
|
||||
'--no-color',
|
||||
];
|
||||
|
||||
if (config.mode == MinerTargetMode.solo) {
|
||||
args.addAll([
|
||||
'--daemon',
|
||||
'-o',
|
||||
'${config.daemonHost}:${config.daemonPort}',
|
||||
'--daemon-zmq-port',
|
||||
config.daemonZmqPort.toString(),
|
||||
]);
|
||||
} else {
|
||||
args.addAll([
|
||||
'-o',
|
||||
'${config.poolHost}:${config.poolPort}',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
final process = await Process.start(
|
||||
binary,
|
||||
args,
|
||||
workingDirectory: p.dirname(binary),
|
||||
mode: ProcessStartMode.detachedWithStdio,
|
||||
);
|
||||
await pidFile.writeAsString(process.pid.toString());
|
||||
unawaited(_pipeLogs(process, launcherLogFile));
|
||||
final earlyExit = await _waitForEarlyExit(process);
|
||||
if (earlyExit != null) {
|
||||
_logger.w('xmrig exited immediately with code $earlyExit');
|
||||
return false;
|
||||
}
|
||||
return _waitForApi(config);
|
||||
} catch (error) {
|
||||
_logger.w('Failed to start xmrig: $error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pipeLogs(Process process, File logFile) async {
|
||||
final sink = logFile.openWrite(mode: FileMode.writeOnlyAppend);
|
||||
sink.writeln('=== ${DateTime.now().toIso8601String()} xmrig launch ===');
|
||||
unawaited(
|
||||
process.stdout
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.forEach((line) => sink.writeln('[stdout] $line')),
|
||||
);
|
||||
unawaited(
|
||||
process.stderr
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.forEach((line) => sink.writeln('[stderr] $line')),
|
||||
);
|
||||
unawaited(
|
||||
process.exitCode.then((code) async {
|
||||
sink.writeln('[exit] $code');
|
||||
await sink.flush();
|
||||
await sink.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int?> _waitForEarlyExit(Process process) async {
|
||||
const grace = Duration(seconds: 2);
|
||||
final result = await Future.any<Object?>([
|
||||
process.exitCode,
|
||||
Future<Object?>.delayed(grace, () => null),
|
||||
]);
|
||||
return result is int ? result : null;
|
||||
}
|
||||
|
||||
Future<bool> _waitForApi(MinerLaunchConfig config) async {
|
||||
for (var i = 0; i < 20; i++) {
|
||||
if (await _isApiAvailable(config)) {
|
||||
return true;
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> _waitForExit(int pid, MinerLaunchConfig? config) async {
|
||||
for (var i = 0; i < 20; i++) {
|
||||
final alive = _isProcessAlive(pid);
|
||||
final apiAlive =
|
||||
config == null ? false : await _isApiAvailable(config);
|
||||
if (!alive && !apiAlive) {
|
||||
return true;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> _isApiAvailable(MinerLaunchConfig config) async {
|
||||
try {
|
||||
final socket = await Socket.connect(
|
||||
'127.0.0.1',
|
||||
config.apiPort,
|
||||
timeout: const Duration(seconds: 1),
|
||||
);
|
||||
socket.destroy();
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _readPid() async {
|
||||
final pidFile = await AppPaths.minerPidFile();
|
||||
if (!await pidFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
return int.tryParse((await pidFile.readAsString()).trim());
|
||||
}
|
||||
|
||||
bool _isProcessAlive(int pid) {
|
||||
if (Platform.isLinux) {
|
||||
return Directory('/proc/$pid').existsSync();
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
try {
|
||||
final result = Process.runSync(
|
||||
'tasklist',
|
||||
['/FI', 'PID eq $pid'],
|
||||
runInShell: true,
|
||||
);
|
||||
return result.stdout.toString().contains('$pid');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String?> _locateBinary() async {
|
||||
final executableDir = p.dirname(Platform.resolvedExecutable);
|
||||
final cwd = Directory.current.path;
|
||||
final name = Platform.isWindows ? 'xmrig.exe' : 'xmrig';
|
||||
final candidates = [
|
||||
p.join(executableDir, 'external', 'miner', name),
|
||||
p.join(cwd, 'external', 'miner', name),
|
||||
p.join(cwd, '..', 'external', 'miner', name),
|
||||
];
|
||||
for (final candidate in candidates) {
|
||||
final file = File(candidate);
|
||||
if (await file.exists()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
double? _readHashrate(List<dynamic> values, int index) {
|
||||
if (index < 0 || index >= values.length) {
|
||||
return null;
|
||||
}
|
||||
return (values[index] as num?)?.toDouble();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
enum MinerTargetMode { solo, pool }
|
||||
|
||||
class MinerLaunchConfig {
|
||||
const MinerLaunchConfig({
|
||||
required this.mode,
|
||||
required this.walletAddress,
|
||||
required this.cpuThreads,
|
||||
required this.apiPort,
|
||||
required this.poolHost,
|
||||
required this.poolPort,
|
||||
required this.daemonHost,
|
||||
required this.daemonPort,
|
||||
required this.daemonZmqPort,
|
||||
});
|
||||
|
||||
final MinerTargetMode mode;
|
||||
final String walletAddress;
|
||||
final int cpuThreads;
|
||||
final int apiPort;
|
||||
final String poolHost;
|
||||
final int poolPort;
|
||||
final String daemonHost;
|
||||
final int daemonPort;
|
||||
final int daemonZmqPort;
|
||||
}
|
||||
|
||||
class XmrigSummary {
|
||||
const XmrigSummary({
|
||||
required this.status,
|
||||
required this.pool,
|
||||
required this.hashrate10s,
|
||||
required this.hashrate1m,
|
||||
required this.hashrate15m,
|
||||
required this.acceptedShares,
|
||||
required this.totalShares,
|
||||
required this.rejectedShares,
|
||||
required this.uptimeSeconds,
|
||||
required this.currentDifficulty,
|
||||
});
|
||||
|
||||
final String status;
|
||||
final String pool;
|
||||
final double? hashrate10s;
|
||||
final double? hashrate1m;
|
||||
final double? hashrate15m;
|
||||
final int acceptedShares;
|
||||
final int totalShares;
|
||||
final int rejectedShares;
|
||||
final int uptimeSeconds;
|
||||
final int currentDifficulty;
|
||||
}
|
||||
|
||||
abstract class MiningService {
|
||||
Future<bool> isRunning({MinerLaunchConfig? config});
|
||||
Future<bool> start({required MinerLaunchConfig config});
|
||||
Future<bool> stop({MinerLaunchConfig? config});
|
||||
Future<bool> restart({required MinerLaunchConfig config});
|
||||
Future<XmrigSummary?> fetchSummary({required MinerLaunchConfig config});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'mining_service.dart';
|
||||
|
||||
class NoopMiningService implements MiningService {
|
||||
@override
|
||||
Future<XmrigSummary?> fetchSummary({required MinerLaunchConfig config}) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isRunning({MinerLaunchConfig? config}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> restart({required MinerLaunchConfig config}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> start({required MinerLaunchConfig config}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stop({MinerLaunchConfig? config}) async {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../domain/models.dart';
|
||||
import '../services/mining_service.dart';
|
||||
import 'wallet_controller.dart';
|
||||
|
||||
class MiningState {
|
||||
const MiningState({
|
||||
required this.running,
|
||||
required this.busy,
|
||||
required this.summary,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
final bool running;
|
||||
final bool busy;
|
||||
final XmrigSummary? summary;
|
||||
final String? error;
|
||||
|
||||
MiningState copyWith({
|
||||
bool? running,
|
||||
bool? busy,
|
||||
XmrigSummary? summary,
|
||||
String? error,
|
||||
}) {
|
||||
return MiningState(
|
||||
running: running ?? this.running,
|
||||
busy: busy ?? this.busy,
|
||||
summary: summary ?? this.summary,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
factory MiningState.initial() {
|
||||
return const MiningState(
|
||||
running: false,
|
||||
busy: false,
|
||||
summary: null,
|
||||
error: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MiningController extends StateNotifier<MiningState> {
|
||||
MiningController({
|
||||
required MiningService service,
|
||||
required Logger logger,
|
||||
required AppConfig Function() readConfig,
|
||||
required WalletState Function() readWalletState,
|
||||
}) : _service = service,
|
||||
_logger = logger,
|
||||
_readConfig = readConfig,
|
||||
_readWalletState = readWalletState,
|
||||
super(MiningState.initial()) {
|
||||
_timer = Timer.periodic(const Duration(seconds: 2), (_) => refresh());
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
final MiningService _service;
|
||||
final Logger _logger;
|
||||
final AppConfig Function() _readConfig;
|
||||
final WalletState Function() _readWalletState;
|
||||
Timer? _timer;
|
||||
|
||||
Future<bool> start() async {
|
||||
return _runAction(() async {
|
||||
final config = _buildLaunchConfig();
|
||||
final started = await _service.start(config: config);
|
||||
if (!started) {
|
||||
throw StateError('Failed to start miner');
|
||||
}
|
||||
await refresh();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> stop() async {
|
||||
return _runAction(() async {
|
||||
final config = _tryBuildLaunchConfig();
|
||||
final stopped = await _service.stop(config: config);
|
||||
if (!stopped) {
|
||||
throw StateError('Failed to stop miner');
|
||||
}
|
||||
await refresh();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> restart() async {
|
||||
return _runAction(() async {
|
||||
final config = _buildLaunchConfig();
|
||||
final restarted = await _service.restart(config: config);
|
||||
if (!restarted) {
|
||||
throw StateError('Failed to restart miner');
|
||||
}
|
||||
await refresh();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
final config = _tryBuildLaunchConfig();
|
||||
if (config == null) {
|
||||
state = state.copyWith(
|
||||
running: false,
|
||||
summary: null,
|
||||
error: state.busy ? state.error : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final running = await _service.isRunning(config: config);
|
||||
final summary = running ? await _service.fetchSummary(config: config) : null;
|
||||
state = state.copyWith(
|
||||
running: running,
|
||||
summary: summary,
|
||||
error: null,
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_logger.w('Failed to refresh miner state', error: error, stackTrace: stack);
|
||||
state = state.copyWith(error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _runAction(Future<bool> Function() action) async {
|
||||
if (state.busy) {
|
||||
return false;
|
||||
}
|
||||
state = state.copyWith(busy: true, error: null);
|
||||
try {
|
||||
return await action();
|
||||
} catch (error, stack) {
|
||||
_logger.w('Mining action failed', error: error, stackTrace: stack);
|
||||
state = state.copyWith(error: error.toString());
|
||||
return false;
|
||||
} finally {
|
||||
state = state.copyWith(busy: false);
|
||||
}
|
||||
}
|
||||
|
||||
MinerLaunchConfig? _tryBuildLaunchConfig() {
|
||||
try {
|
||||
return _buildLaunchConfig();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MinerLaunchConfig _buildLaunchConfig() {
|
||||
final config = _readConfig();
|
||||
final wallet = _readWalletState().walletInfo;
|
||||
final address = wallet?.address.trim() ?? '';
|
||||
if (address.isEmpty) {
|
||||
throw StateError('Open a wallet before starting the miner');
|
||||
}
|
||||
|
||||
final mining = config.miningConfig;
|
||||
final localNode = _resolveLocalNodePorts(config.localNodeArgs);
|
||||
final mode =
|
||||
mining.mode == MiningMode.solo ? MinerTargetMode.solo : MinerTargetMode.pool;
|
||||
|
||||
if (mode == MinerTargetMode.solo && config.nodeConfig.mode != NodeMode.local) {
|
||||
throw StateError('Solo mining requires the wallet to use the local node');
|
||||
}
|
||||
|
||||
return MinerLaunchConfig(
|
||||
mode: mode,
|
||||
walletAddress: address,
|
||||
cpuThreads: mining.cpuThreads.clamp(1, 64),
|
||||
apiPort: mining.apiPort,
|
||||
poolHost: mining.poolHost,
|
||||
poolPort: mining.poolPort,
|
||||
daemonHost: localNode.host,
|
||||
daemonPort: localNode.rpcPort,
|
||||
daemonZmqPort: localNode.zmqPort,
|
||||
);
|
||||
}
|
||||
|
||||
({String host, int rpcPort, int zmqPort}) _resolveLocalNodePorts(
|
||||
List<String> args,
|
||||
) {
|
||||
var host = '127.0.0.1';
|
||||
var rpcPort = 17750;
|
||||
var zmqPort = 17751;
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
final arg = args[i];
|
||||
if (arg.startsWith('--rpc-bind-ip=')) {
|
||||
host = arg.split('=').last.trim();
|
||||
} else if (arg == '--rpc-bind-ip' && i + 1 < args.length) {
|
||||
host = args[i + 1].trim();
|
||||
} else if (arg.startsWith('--rpc-bind-port=')) {
|
||||
rpcPort = int.tryParse(arg.split('=').last.trim()) ?? rpcPort;
|
||||
} else if (arg == '--rpc-bind-port' && i + 1 < args.length) {
|
||||
rpcPort = int.tryParse(args[i + 1].trim()) ?? rpcPort;
|
||||
} else if (arg.startsWith('--zmq-rpc-bind-port=')) {
|
||||
zmqPort = int.tryParse(arg.split('=').last.trim()) ?? zmqPort;
|
||||
} else if (arg == '--zmq-rpc-bind-port' && i + 1 < args.length) {
|
||||
zmqPort = int.tryParse(args[i + 1].trim()) ?? zmqPort;
|
||||
}
|
||||
}
|
||||
|
||||
if (host == '0.0.0.0' || host == '::' || host == '[::]') {
|
||||
host = '127.0.0.1';
|
||||
}
|
||||
return (host: host, rpcPort: rpcPort, zmqPort: zmqPort);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,18 @@ import '../domain/models.dart';
|
||||
import '../native/wallet_backend.dart';
|
||||
import '../native/wallet_backend_factory.dart';
|
||||
import '../services/linux_local_node_service.dart';
|
||||
import '../services/bundled_mining_service.dart';
|
||||
import '../services/local_node_service.dart';
|
||||
import '../services/mining_service.dart';
|
||||
import '../services/noop_local_node_service.dart';
|
||||
import '../services/noop_mining_service.dart';
|
||||
import '../services/noop_tray_service.dart';
|
||||
import '../services/sync_scheduler.dart';
|
||||
import '../services/tray_service.dart';
|
||||
import '../services/windows_local_node_service.dart';
|
||||
import '../services/window_lifecycle_service.dart';
|
||||
import 'app_config_controller.dart';
|
||||
import 'mining_controller.dart';
|
||||
import 'wallet_controller.dart';
|
||||
|
||||
final loggerProvider = Provider<Logger>((ref) => Logger());
|
||||
@@ -75,6 +79,24 @@ final localNodeServiceProvider = Provider<LocalNodeService>((ref) {
|
||||
return NoopLocalNodeService();
|
||||
});
|
||||
|
||||
final miningServiceProvider = Provider<MiningService>((ref) {
|
||||
final logger = ref.watch(loggerProvider);
|
||||
if (Platform.isLinux || Platform.isWindows) {
|
||||
return BundledMiningService(logger: logger);
|
||||
}
|
||||
return NoopMiningService();
|
||||
});
|
||||
|
||||
final miningControllerProvider =
|
||||
StateNotifierProvider<MiningController, MiningState>((ref) {
|
||||
return MiningController(
|
||||
service: ref.watch(miningServiceProvider),
|
||||
logger: ref.watch(loggerProvider),
|
||||
readConfig: () => ref.read(appConfigControllerProvider),
|
||||
readWalletState: () => ref.read(walletControllerProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final windowLifecycleServiceProvider = Provider<WindowLifecycleService>((ref) {
|
||||
final tray = ref.read(trayServiceProvider);
|
||||
return WindowLifecycleService(
|
||||
|
||||
+237
-141
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
@@ -43,6 +44,7 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
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(
|
||||
@@ -67,138 +69,159 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Column(
|
||||
children: [
|
||||
_SectionCard(
|
||||
title: l10n.miningControlsTitle,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Symbols.play_arrow),
|
||||
label: Text(l10n.miningStartAction),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Symbols.stop),
|
||||
label: Text(l10n.miningStopAction),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: null,
|
||||
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(
|
||||
l10n.miningBackendPending,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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
|
||||
: '${config.miningConfig.poolHost}:${config.miningConfig.poolPort}',
|
||||
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: 16),
|
||||
_SectionCard(
|
||||
title: l10n.miningStatsTitle,
|
||||
child: Wrap(
|
||||
_SectionCard(
|
||||
title: l10n.miningControlsTitle,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_StatTile(
|
||||
label: l10n.transactionStatusLabel,
|
||||
value: l10n.comingSoon,
|
||||
FilledButton.icon(
|
||||
onPressed: miningState.busy || miningState.running
|
||||
? null
|
||||
: _startMining,
|
||||
icon: const Icon(Symbols.play_arrow),
|
||||
label: Text(l10n.miningStartAction),
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningHashrate10sLabel,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
OutlinedButton.icon(
|
||||
onPressed: miningState.busy || !miningState.running
|
||||
? null
|
||||
: _stopMining,
|
||||
icon: const Icon(Symbols.stop),
|
||||
label: Text(l10n.miningStopAction),
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningHashrate1mLabel,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
OutlinedButton.icon(
|
||||
onPressed: miningState.busy || !miningState.running
|
||||
? null
|
||||
: _restartMining,
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text(l10n.localNodeRestartAction),
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningLabelHashrate15m,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningJobsLabel,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningAcceptedSharesLabel,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningRejectedSharesLabel,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
),
|
||||
_StatTile(
|
||||
label: l10n.miningLabelUptime,
|
||||
value: l10n.miningDataUnknownValue,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -242,6 +265,7 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.miningSettingsSaved)),
|
||||
);
|
||||
await ref.read(miningControllerProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
Future<void> _showSettingsDialog(int maxThreads) async {
|
||||
@@ -257,6 +281,7 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
width: 520,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
final nodeMode = ref.read(appConfigControllerProvider).nodeConfig.mode;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -297,13 +322,11 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
ref.read(appConfigControllerProvider).nodeConfig.mode ==
|
||||
NodeMode.local
|
||||
nodeMode == NodeMode.local
|
||||
? l10n.miningSoloHint
|
||||
: l10n.miningSoloNeedsLocalNode,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: ref.read(appConfigControllerProvider).nodeConfig.mode ==
|
||||
NodeMode.local
|
||||
color: nodeMode == NodeMode.local
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.error,
|
||||
),
|
||||
@@ -392,6 +415,76 @@ class _MiningScreenState extends ConsumerState<MiningScreen> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startMining() async {
|
||||
final l10n = context.l10n;
|
||||
final started = await ref.read(miningControllerProvider.notifier).start();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
started ? l10n.miningMinerStartSuccess : l10n.miningMinerStartFailure,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _stopMining() async {
|
||||
final l10n = context.l10n;
|
||||
final stopped = await ref.read(miningControllerProvider.notifier).stop();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
stopped ? l10n.miningMinerStopSuccess : l10n.miningMinerStopFailure,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restartMining() async {
|
||||
final l10n = context.l10n;
|
||||
final restarted = await ref.read(miningControllerProvider.notifier).restart();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
restarted
|
||||
? l10n.miningMinerRestartSuccess
|
||||
: 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 {
|
||||
@@ -403,21 +496,24 @@ class _SectionCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
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,
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user