From b4d32e5ea82bd1bc85b5b88ef288326995185a35 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Mon, 20 Apr 2026 22:42:52 +0200 Subject: [PATCH] Split peyawallet into lite and mining variants --- .gitea/workflows/build.yml | 78 +++- .gitea/workflows/release.yml | 78 +++- .gitignore | 2 + lib/app_features.dart | 13 + lib/domain/models.dart | 72 ++++ lib/l10n/app_en.arb | 25 ++ lib/l10n/app_localizations.dart | 150 ++++++++ lib/l10n/app_localizations_en.dart | 83 +++++ lib/l10n/app_localizations_pl.dart | 83 +++++ lib/l10n/app_pl.arb | 25 ++ lib/state/app_config_controller.dart | 42 +++ lib/ui/screens/home_shell.dart | 102 ++++-- lib/ui/screens/mining_screen.dart | 527 ++++++++++++++++++++++++++- scripts/ci/stage_release_runtime.py | 53 ++- 14 files changed, 1263 insertions(+), 70 deletions(-) create mode 100644 lib/app_features.dart diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index ff509cf..bb4a344 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,5 +1,5 @@ name: build -run-name: build wallet (peya=${{ inputs.peya_release_tag || 'latest' }}, monero_c=${{ inputs.monero_c_release_tag || 'latest' }}) +run-name: build wallet variants (peya=${{ inputs.peya_release_tag || 'latest' }}, monero_c=${{ inputs.monero_c_release_tag || 'latest' }}, xmrig=${{ inputs.xmrig_release_tag || 'latest' }}) on: workflow_dispatch: @@ -12,6 +12,10 @@ on: description: monero_c release tag to bundle runtime from required: false default: latest + xmrig_release_tag: + description: xmrig release tag to bundle mining runtime from + required: false + default: latest push: branches: - develop @@ -29,11 +33,20 @@ on: jobs: build-linux: - name: Build Linux wallet + name: Build Linux (${{ matrix.flavor }}) runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - flavor: lite + enable_mining: "false" + - flavor: mining + enable_mining: "true" env: PEYA_RELEASE_TAG: ${{ inputs.peya_release_tag }} MONERO_C_RELEASE_TAG: ${{ inputs.monero_c_release_tag }} + XMRIG_RELEASE_TAG: ${{ inputs.xmrig_release_tag }} steps: - name: Checkout uses: https://github.com/actions/checkout@v4 @@ -82,33 +95,56 @@ jobs: MONERO_C_GITEA_PAT: ${{ secrets.MONERO_C_GITEA_PAT }} GITEA_PAT: ${{ secrets.PEYA_GITEA_PAT }} run: | + set -euo pipefail + EXTRA_ARGS=() + if [ "${{ matrix.flavor }}" = "mining" ]; then + EXTRA_ARGS+=(--xmrig-tag "${XMRIG_RELEASE_TAG:-latest}") + fi python3 scripts/ci/stage_release_runtime.py \ --platform linux \ --peya-tag "${PEYA_RELEASE_TAG:-latest}" \ --monero-c-tag "${MONERO_C_RELEASE_TAG:-latest}" \ - --repo-root . + --repo-root . \ + "${EXTRA_ARGS[@]}" - name: Fetch Dart/Flutter deps - run: | - flutter pub get + run: flutter pub get - name: Build Linux release run: | - flutter build linux --release + flutter build linux --release \ + --dart-define=PEYA_ENABLE_MINING=${{ matrix.enable_mining }} \ + --dart-define=PEYA_RELEASE_FLAVOR=${{ matrix.flavor }} + + - name: Bundle xmrig + if: ${{ matrix.flavor == 'mining' }} + run: | + set -euo pipefail + install -Dm755 external/miner/xmrig \ + build/linux/x64/release/bundle/external/miner/xmrig - name: Upload Linux bundle uses: https://github.com/actions/upload-artifact@v3 with: - name: peyawallet-linux-release + name: peyawallet-${{ matrix.flavor }}-linux-release path: build/linux/x64/release/bundle/** if-no-files-found: error build-windows: - name: Build Windows wallet + name: Build Windows (${{ matrix.flavor }}) runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - flavor: lite + enable_mining: "false" + - flavor: mining + enable_mining: "true" env: PEYA_RELEASE_TAG: ${{ inputs.peya_release_tag }} MONERO_C_RELEASE_TAG: ${{ inputs.monero_c_release_tag }} + XMRIG_RELEASE_TAG: ${{ inputs.xmrig_release_tag }} steps: - name: Checkout uses: https://github.com/actions/checkout@v4 @@ -140,30 +176,42 @@ jobs: MONERO_C_GITEA_PAT: ${{ secrets.PEYA_GITEA_PAT }} run: | set -euo pipefail + EXTRA_ARGS=() + if [ "${{ matrix.flavor }}" = "mining" ]; then + EXTRA_ARGS+=(--xmrig-tag "${XMRIG_RELEASE_TAG:-latest}") + fi python -u scripts/ci/stage_release_runtime.py \ --platform windows \ --peya-tag "${PEYA_RELEASE_TAG:-latest}" \ --monero-c-tag "${MONERO_C_RELEASE_TAG:-latest}" \ - --repo-root . + --repo-root . \ + "${EXTRA_ARGS[@]}" - name: Show toolchain shell: bash - run: | - flutter doctor -v + run: flutter doctor -v - name: Fetch Dart/Flutter deps shell: bash - run: | - flutter pub get + run: flutter pub get - name: Build Windows release shell: bash run: | - flutter build windows --release + flutter build windows --release \ + --dart-define=PEYA_ENABLE_MINING=${{ matrix.enable_mining }} \ + --dart-define=PEYA_RELEASE_FLAVOR=${{ matrix.flavor }} + + - name: Bundle xmrig + if: ${{ matrix.flavor == 'mining' }} + shell: cmd + run: | + if not exist build\windows\x64\runner\Release\external\miner mkdir build\windows\x64\runner\Release\external\miner + copy external\miner\xmrig.exe build\windows\x64\runner\Release\external\miner\xmrig.exe - name: Upload Windows bundle uses: https://github.com/actions/upload-artifact@v3 with: - name: peyawallet-windows-release + name: peyawallet-${{ matrix.flavor }}-windows-release path: build/windows/x64/runner/Release/** if-no-files-found: error diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 46518ab..aba9560 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,5 +1,5 @@ name: release -run-name: release wallet ${{ inputs.tag_name }} +run-name: release wallet variants ${{ inputs.tag_name }} on: workflow_dispatch: @@ -19,6 +19,10 @@ on: description: monero_c release tag to bundle runtime from required: false default: latest + xmrig_release_tag: + description: xmrig release tag to bundle mining runtime from + required: false + default: latest release_name: description: Optional release title required: false @@ -121,12 +125,21 @@ jobs: echo "release_id=$(jq -r '.id' /tmp/release.json)" >> "$GITHUB_OUTPUT" build-linux: - name: Build Linux release + name: Build Linux release (${{ matrix.flavor }}) needs: create-release runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - flavor: lite + enable_mining: "false" + - flavor: mining + enable_mining: "true" env: PEYA_RELEASE_TAG: ${{ inputs.peya_release_tag }} MONERO_C_RELEASE_TAG: ${{ inputs.monero_c_release_tag }} + XMRIG_RELEASE_TAG: ${{ inputs.xmrig_release_tag }} steps: - name: Checkout uses: https://github.com/actions/checkout@v4 @@ -178,26 +191,40 @@ jobs: MONERO_C_GITEA_PAT: ${{ secrets.MONERO_C_GITEA_PAT }} GITEA_PAT: ${{ secrets.GITEA_PAT }} run: | + set -euo pipefail + EXTRA_ARGS=() + if [ "${{ matrix.flavor }}" = "mining" ]; then + EXTRA_ARGS+=(--xmrig-tag "${XMRIG_RELEASE_TAG:-latest}") + fi python3 scripts/ci/stage_release_runtime.py \ --platform linux \ --peya-tag "${PEYA_RELEASE_TAG:-latest}" \ --monero-c-tag "${MONERO_C_RELEASE_TAG:-latest}" \ - --repo-root . + --repo-root . \ + "${EXTRA_ARGS[@]}" - name: Fetch Dart/Flutter deps - run: | - flutter pub get + run: flutter pub get - name: Build Linux release run: | - flutter build linux --release + flutter build linux --release \ + --dart-define=PEYA_ENABLE_MINING=${{ matrix.enable_mining }} \ + --dart-define=PEYA_RELEASE_FLAVOR=${{ matrix.flavor }} + + - name: Bundle xmrig + if: ${{ matrix.flavor == 'mining' }} + run: | + set -euo pipefail + install -Dm755 external/miner/xmrig \ + build/linux/x64/release/bundle/external/miner/xmrig - name: Package Linux release env: TAG_NAME: ${{ inputs.tag_name }} run: | set -euo pipefail - archive="/tmp/peyawallet-${TAG_NAME}-linux-x86_64.tar.gz" + archive="/tmp/peyawallet-${{ matrix.flavor }}-${TAG_NAME}-linux-x86_64.tar.gz" tar -C build/linux/x64/release/bundle -czf "${archive}" . echo "ARCHIVE_PATH=${archive}" >> "$GITHUB_ENV" echo "ARCHIVE_NAME=$(basename "${archive}")" >> "$GITHUB_ENV" @@ -244,12 +271,21 @@ jobs: >/dev/null build-windows: - name: Build Windows release + name: Build Windows release (${{ matrix.flavor }}) needs: create-release runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - flavor: lite + enable_mining: "false" + - flavor: mining + enable_mining: "true" env: PEYA_RELEASE_TAG: ${{ inputs.peya_release_tag }} MONERO_C_RELEASE_TAG: ${{ inputs.monero_c_release_tag }} + XMRIG_RELEASE_TAG: ${{ inputs.xmrig_release_tag }} steps: - name: Checkout uses: https://github.com/actions/checkout@v4 @@ -282,26 +318,38 @@ jobs: GITEA_PAT: ${{ secrets.GITEA_PAT }} run: | set -euo pipefail + EXTRA_ARGS=() + if [ "${{ matrix.flavor }}" = "mining" ]; then + EXTRA_ARGS+=(--xmrig-tag "${XMRIG_RELEASE_TAG:-latest}") + fi python -u scripts/ci/stage_release_runtime.py \ --platform windows \ --peya-tag "${PEYA_RELEASE_TAG:-latest}" \ --monero-c-tag "${MONERO_C_RELEASE_TAG:-latest}" \ - --repo-root . + --repo-root . \ + "${EXTRA_ARGS[@]}" - name: Show toolchain shell: bash - run: | - flutter doctor -v + run: flutter doctor -v - name: Fetch Dart/Flutter deps shell: bash - run: | - flutter pub get + run: flutter pub get - name: Build Windows release shell: bash run: | - flutter build windows --release + flutter build windows --release \ + --dart-define=PEYA_ENABLE_MINING=${{ matrix.enable_mining }} \ + --dart-define=PEYA_RELEASE_FLAVOR=${{ matrix.flavor }} + + - name: Bundle xmrig + if: ${{ matrix.flavor == 'mining' }} + shell: cmd + run: | + if not exist build\windows\x64\runner\Release\external\miner mkdir build\windows\x64\runner\Release\external\miner + copy external\miner\xmrig.exe build\windows\x64\runner\Release\external\miner\xmrig.exe - name: Package Windows release shell: bash @@ -309,7 +357,7 @@ jobs: TAG_NAME: ${{ inputs.tag_name }} run: | set -euo pipefail - archive="C:/Windows/Temp/peyawallet-${TAG_NAME}-windows-x86_64.zip" + archive="C:/Windows/Temp/peyawallet-${{ matrix.flavor }}-${TAG_NAME}-windows-x86_64.zip" powershell.exe -NoProfile -Command "if (Test-Path '${archive}') { Remove-Item '${archive}' -Force }; Compress-Archive -Path 'build/windows/x64/runner/Release/*' -DestinationPath '${archive}'" echo "ARCHIVE_PATH=${archive}" >> "$GITHUB_ENV" echo "ARCHIVE_NAME=$(basename "${archive}")" >> "$GITHUB_ENV" diff --git a/.gitignore b/.gitignore index e549893..e1ea2ec 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ migrate_working_dir/ .pub/ /build/ /external/ +/third_party/daemon/ +/third_party/monero_c/ # Local working files codex_resume.txt diff --git a/lib/app_features.dart b/lib/app_features.dart new file mode 100644 index 0000000..18b72bc --- /dev/null +++ b/lib/app_features.dart @@ -0,0 +1,13 @@ +abstract final class AppFeatures { + static const bool enableMining = bool.fromEnvironment( + 'PEYA_ENABLE_MINING', + defaultValue: true, + ); + + static const String releaseFlavor = String.fromEnvironment( + 'PEYA_RELEASE_FLAVOR', + defaultValue: 'mining', + ); + + static bool get isMiningFlavor => enableMining; +} diff --git a/lib/domain/models.dart b/lib/domain/models.dart index 6a81b14..e94006a 100644 --- a/lib/domain/models.dart +++ b/lib/domain/models.dart @@ -6,6 +6,8 @@ enum NodeMode { local, remote } enum LanguagePreference { system, english, polish } +enum MiningMode { solo, pool } + class RemoteNode { const RemoteNode({ required this.host, @@ -223,6 +225,61 @@ class SyncStatus { } } +class MiningConfig { + const MiningConfig({ + required this.mode, + required this.cpuThreads, + required this.poolHost, + required this.poolPort, + required this.apiPort, + }); + + final MiningMode mode; + final int cpuThreads; + final String poolHost; + final int poolPort; + final int apiPort; + + MiningConfig copyWith({ + MiningMode? mode, + int? cpuThreads, + String? poolHost, + int? poolPort, + int? apiPort, + }) { + return MiningConfig( + mode: mode ?? this.mode, + cpuThreads: cpuThreads ?? this.cpuThreads, + poolHost: poolHost ?? this.poolHost, + poolPort: poolPort ?? this.poolPort, + apiPort: apiPort ?? this.apiPort, + ); + } + + Map toJson() { + return { + 'mode': mode.name, + 'cpuThreads': cpuThreads, + 'poolHost': poolHost, + 'poolPort': poolPort, + 'apiPort': apiPort, + }; + } + + factory MiningConfig.fromJson(Map json) { + return MiningConfig( + mode: MiningMode.values.firstWhere( + (value) => value.name == (json['mode'] as String? ?? 'solo'), + orElse: () => MiningMode.solo, + ), + cpuThreads: (json['cpuThreads'] as num?)?.toInt() ?? 1, + poolHost: json['poolHost'] as String? ?? 'peya.cryptohash.top', + poolPort: (json['poolPort'] as num?)?.toInt() ?? 3333, + apiPort: (json['apiPort'] as num?)?.toInt() ?? 18091, + ); + } +} + class AppConfig { const AppConfig({ required this.lastWallet, @@ -235,6 +292,7 @@ class AppConfig { required this.languagePreference, required this.hiddenSubaddresses, required this.localNodeArgs, + required this.miningConfig, required this.p2poolStratum, required this.p2poolP2p, required this.p2poolStartMining, @@ -251,6 +309,7 @@ class AppConfig { final LanguagePreference languagePreference; final Map> hiddenSubaddresses; final List localNodeArgs; + final MiningConfig miningConfig; final String p2poolStratum; final String p2poolP2p; final bool p2poolStartMining; @@ -267,6 +326,7 @@ class AppConfig { LanguagePreference? languagePreference, Map>? hiddenSubaddresses, List? localNodeArgs, + MiningConfig? miningConfig, String? p2poolStratum, String? p2poolP2p, bool? p2poolStartMining, @@ -283,6 +343,7 @@ class AppConfig { languagePreference: languagePreference ?? this.languagePreference, hiddenSubaddresses: hiddenSubaddresses ?? this.hiddenSubaddresses, localNodeArgs: localNodeArgs ?? this.localNodeArgs, + miningConfig: miningConfig ?? this.miningConfig, p2poolStratum: p2poolStratum ?? this.p2poolStratum, p2poolP2p: p2poolP2p ?? this.p2poolP2p, p2poolStartMining: p2poolStartMining ?? this.p2poolStartMining, @@ -306,6 +367,13 @@ class AppConfig { languagePreference: LanguagePreference.system, hiddenSubaddresses: const {}, localNodeArgs: const [], + miningConfig: const MiningConfig( + mode: MiningMode.solo, + cpuThreads: 1, + poolHost: 'peya.cryptohash.top', + poolPort: 3333, + apiPort: 18091, + ), p2poolStratum: '0.0.0.0:3333', p2poolP2p: '0.0.0.0:38889', p2poolStartMining: false, @@ -325,6 +393,7 @@ class AppConfig { 'languagePreference': languagePreference.name, 'hiddenSubaddresses': hiddenSubaddresses, 'localNodeArgs': localNodeArgs, + 'miningConfig': miningConfig.toJson(), 'p2poolStratum': p2poolStratum, 'p2poolP2p': p2poolP2p, 'p2poolStartMining': p2poolStartMining, @@ -349,6 +418,8 @@ class AppConfig { final p2poolStartMining = json['p2poolStartMining'] as bool? ?? false; final p2poolMiningThreads = (json['p2poolMiningThreads'] as num?)?.toInt() ?? 1; + final miningConfigJson = + json['miningConfig'] as Map? ?? const {}; return AppConfig( lastWallet: json['lastWallet'] == null ? null @@ -374,6 +445,7 @@ class AppConfig { ), hiddenSubaddresses: hidden, localNodeArgs: localNodeArgs, + miningConfig: MiningConfig.fromJson(miningConfigJson), p2poolStratum: p2poolStratum, p2poolP2p: p2poolP2p, p2poolStartMining: p2poolStartMining, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6fc1d4d..7664c90 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -345,6 +345,31 @@ "miningThreadsLabel": "CPU threads", "miningSettingsSaved": "Mining settings saved.", "miningCurrentConfigTitle": "Current configuration", + "miningScreenSubtitle": "Configure the CPU miner release now. Process control and live XMRig stats land in the next step.", + "miningSourceTitle": "Source", + "miningModeLabel": "Mining mode", + "miningModeSolo": "Solo to local daemon", + "miningModePool": "Pool", + "miningTargetLabel": "Target", + "miningSoloTargetValue": "127.0.0.1:17750 (local Peya daemon)", + "miningSoloHint": "Solo mode will target the local daemon bundled with the wallet.", + "miningSoloNeedsLocalNode": "Solo mode expects the wallet to use the local node.", + "miningPoolHint": "Default pool is preconfigured for Peya and can be adjusted here.", + "miningApiPortLabel": "XMRig API port", + "miningResetDraftAction": "Reset draft", + "miningConfigInvalid": "Enter a valid pool host and ports.", + "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", + "miningHashrate10sLabel": "Hashrate (10s)", + "miningHashrate1mLabel": "Hashrate (1m)", + "miningJobsLabel": "Jobs", + "miningAcceptedSharesLabel": "Accepted shares", + "miningRejectedSharesLabel": "Rejected shares", + "miningBundledTitle": "Bundled miner", + "miningBundledEnabled": "This build includes the bundled XMRig binary.", + "miningBundledDisabled": "This release flavor does not include the bundled miner.", + "miningBundledBody": "Mining releases bundle XMRig from the dedicated Peya xmrig repo. Lite releases hide the tab entirely.", "miningStartMiningEnabled": "Enabled ({threads} threads)", "@miningStartMiningEnabled": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 10dfe68..128ae37 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1694,6 +1694,156 @@ abstract class AppLocalizations { /// **'Current configuration'** String get miningCurrentConfigTitle; + /// No description provided for @miningScreenSubtitle. + /// + /// In en, this message translates to: + /// **'Configure the CPU miner release now. Process control and live XMRig stats land in the next step.'** + String get miningScreenSubtitle; + + /// No description provided for @miningSourceTitle. + /// + /// In en, this message translates to: + /// **'Source'** + String get miningSourceTitle; + + /// No description provided for @miningModeLabel. + /// + /// In en, this message translates to: + /// **'Mining mode'** + String get miningModeLabel; + + /// No description provided for @miningModeSolo. + /// + /// In en, this message translates to: + /// **'Solo to local daemon'** + String get miningModeSolo; + + /// No description provided for @miningModePool. + /// + /// In en, this message translates to: + /// **'Pool'** + String get miningModePool; + + /// No description provided for @miningTargetLabel. + /// + /// In en, this message translates to: + /// **'Target'** + String get miningTargetLabel; + + /// No description provided for @miningSoloTargetValue. + /// + /// In en, this message translates to: + /// **'127.0.0.1:17750 (local Peya daemon)'** + String get miningSoloTargetValue; + + /// No description provided for @miningSoloHint. + /// + /// In en, this message translates to: + /// **'Solo mode will target the local daemon bundled with the wallet.'** + String get miningSoloHint; + + /// No description provided for @miningSoloNeedsLocalNode. + /// + /// In en, this message translates to: + /// **'Solo mode expects the wallet to use the local node.'** + String get miningSoloNeedsLocalNode; + + /// No description provided for @miningPoolHint. + /// + /// In en, this message translates to: + /// **'Default pool is preconfigured for Peya and can be adjusted here.'** + String get miningPoolHint; + + /// No description provided for @miningApiPortLabel. + /// + /// In en, this message translates to: + /// **'XMRig API port'** + String get miningApiPortLabel; + + /// No description provided for @miningResetDraftAction. + /// + /// In en, this message translates to: + /// **'Reset draft'** + String get miningResetDraftAction; + + /// No description provided for @miningConfigInvalid. + /// + /// In en, this message translates to: + /// **'Enter a valid pool host and ports.'** + String get miningConfigInvalid; + + /// No description provided for @miningControlsTitle. + /// + /// In en, this message translates to: + /// **'Controls'** + String get miningControlsTitle; + + /// No description provided for @miningBackendPending. + /// + /// In en, this message translates to: + /// **'Miner process control will be enabled in the next step. This screen already stores the target config and release layout.'** + String get miningBackendPending; + + /// No description provided for @miningStatsTitle. + /// + /// In en, this message translates to: + /// **'Stats'** + String get miningStatsTitle; + + /// No description provided for @miningHashrate10sLabel. + /// + /// In en, this message translates to: + /// **'Hashrate (10s)'** + String get miningHashrate10sLabel; + + /// No description provided for @miningHashrate1mLabel. + /// + /// In en, this message translates to: + /// **'Hashrate (1m)'** + String get miningHashrate1mLabel; + + /// No description provided for @miningJobsLabel. + /// + /// In en, this message translates to: + /// **'Jobs'** + String get miningJobsLabel; + + /// No description provided for @miningAcceptedSharesLabel. + /// + /// In en, this message translates to: + /// **'Accepted shares'** + String get miningAcceptedSharesLabel; + + /// No description provided for @miningRejectedSharesLabel. + /// + /// In en, this message translates to: + /// **'Rejected shares'** + String get miningRejectedSharesLabel; + + /// No description provided for @miningBundledTitle. + /// + /// In en, this message translates to: + /// **'Bundled miner'** + String get miningBundledTitle; + + /// No description provided for @miningBundledEnabled. + /// + /// In en, this message translates to: + /// **'This build includes the bundled XMRig binary.'** + String get miningBundledEnabled; + + /// No description provided for @miningBundledDisabled. + /// + /// In en, this message translates to: + /// **'This release flavor does not include the bundled miner.'** + String get miningBundledDisabled; + + /// No description provided for @miningBundledBody. + /// + /// In en, this message translates to: + /// **'Mining releases bundle XMRig from the dedicated Peya xmrig repo. Lite releases hide the tab entirely.'** + String get miningBundledBody; + /// No description provided for @miningStartMiningEnabled. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0921a3f..daf5b33 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -850,6 +850,89 @@ class AppLocalizationsEn extends AppLocalizations { @override String get miningCurrentConfigTitle => 'Current configuration'; + @override + String get miningScreenSubtitle => + 'Configure the CPU miner release now. Process control and live XMRig stats land in the next step.'; + + @override + String get miningSourceTitle => 'Source'; + + @override + String get miningModeLabel => 'Mining mode'; + + @override + String get miningModeSolo => 'Solo to local daemon'; + + @override + String get miningModePool => 'Pool'; + + @override + String get miningTargetLabel => 'Target'; + + @override + String get miningSoloTargetValue => '127.0.0.1:17750 (local Peya daemon)'; + + @override + String get miningSoloHint => + 'Solo mode will target the local daemon bundled with the wallet.'; + + @override + String get miningSoloNeedsLocalNode => + 'Solo mode expects the wallet to use the local node.'; + + @override + String get miningPoolHint => + 'Default pool is preconfigured for Peya and can be adjusted here.'; + + @override + String get miningApiPortLabel => 'XMRig API port'; + + @override + String get miningResetDraftAction => 'Reset draft'; + + @override + String get miningConfigInvalid => 'Enter a valid pool host and ports.'; + + @override + String get miningControlsTitle => 'Controls'; + + @override + String get miningBackendPending => + 'Miner process control will be enabled in the next step. This screen already stores the target config and release layout.'; + + @override + String get miningStatsTitle => 'Stats'; + + @override + String get miningHashrate10sLabel => 'Hashrate (10s)'; + + @override + String get miningHashrate1mLabel => 'Hashrate (1m)'; + + @override + String get miningJobsLabel => 'Jobs'; + + @override + String get miningAcceptedSharesLabel => 'Accepted shares'; + + @override + String get miningRejectedSharesLabel => 'Rejected shares'; + + @override + String get miningBundledTitle => 'Bundled miner'; + + @override + String get miningBundledEnabled => + 'This build includes the bundled XMRig binary.'; + + @override + String get miningBundledDisabled => + 'This release flavor does not include the bundled miner.'; + + @override + String get miningBundledBody => + 'Mining releases bundle XMRig from the dedicated Peya xmrig repo. Lite releases hide the tab entirely.'; + @override String miningStartMiningEnabled(Object threads) { return 'Enabled ($threads threads)'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 435b082..9512fa1 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -854,6 +854,89 @@ class AppLocalizationsPl extends AppLocalizations { @override String get miningCurrentConfigTitle => 'Aktualna konfiguracja'; + @override + String get miningScreenSubtitle => + 'Tu konfigurujesz wariant CPU miner. Sterowanie procesem i żywe statystyki XMRig dojdą w następnym kroku.'; + + @override + String get miningSourceTitle => 'Źródło'; + + @override + String get miningModeLabel => 'Tryb kopania'; + + @override + String get miningModeSolo => 'Solo do lokalnego daemona'; + + @override + String get miningModePool => 'Pool'; + + @override + String get miningTargetLabel => 'Cel'; + + @override + String get miningSoloTargetValue => '127.0.0.1:17750 (lokalny daemon Peya)'; + + @override + String get miningSoloHint => + 'Tryb solo będzie kierował kopanie do lokalnego daemona bundlowanego z walletem.'; + + @override + String get miningSoloNeedsLocalNode => + 'Tryb solo zakłada, że portfel korzysta z lokalnego noda.'; + + @override + String get miningPoolHint => + 'Domyślny pool dla Peya jest już wpisany i można go tu zmienić.'; + + @override + String get miningApiPortLabel => 'Port API XMRig'; + + @override + String get miningResetDraftAction => 'Przywróć szkic'; + + @override + String get miningConfigInvalid => 'Podaj poprawny host poola i porty.'; + + @override + String get miningControlsTitle => 'Sterowanie'; + + @override + String get miningBackendPending => + 'Sterowanie procesem minera zostanie dopięte w następnym kroku. Ten ekran zapisuje już docelową konfigurację i układ releasu.'; + + @override + String get miningStatsTitle => 'Statystyki'; + + @override + String get miningHashrate10sLabel => 'Hashrate (10s)'; + + @override + String get miningHashrate1mLabel => 'Hashrate (1m)'; + + @override + String get miningJobsLabel => 'Joby'; + + @override + String get miningAcceptedSharesLabel => 'Zaakceptowane udziały'; + + @override + String get miningRejectedSharesLabel => 'Odrzucone udziały'; + + @override + String get miningBundledTitle => 'Bundlowany miner'; + + @override + String get miningBundledEnabled => + 'Ten build zawiera bundlowaną binarkę XMRig.'; + + @override + String get miningBundledDisabled => + 'Ten wariant releasu nie zawiera bundlowanego minera.'; + + @override + String get miningBundledBody => + 'Warianty mining bundle\'ują XMRig z dedykowanego repo Peya xmrig. Wariant lite całkowicie ukrywa ten tab.'; + @override String miningStartMiningEnabled(Object threads) { return 'Włączony ($threads wątków)'; diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index bfa6da1..c2729a8 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -345,6 +345,31 @@ "miningThreadsLabel": "Wątki CPU", "miningSettingsSaved": "Ustawienia mining zapisane.", "miningCurrentConfigTitle": "Aktualna konfiguracja", + "miningScreenSubtitle": "Tu konfigurujesz wariant CPU miner. Sterowanie procesem i żywe statystyki XMRig dojdą w następnym kroku.", + "miningSourceTitle": "Źródło", + "miningModeLabel": "Tryb kopania", + "miningModeSolo": "Solo do lokalnego daemona", + "miningModePool": "Pool", + "miningTargetLabel": "Cel", + "miningSoloTargetValue": "127.0.0.1:17750 (lokalny daemon Peya)", + "miningSoloHint": "Tryb solo będzie kierował kopanie do lokalnego daemona bundlowanego z walletem.", + "miningSoloNeedsLocalNode": "Tryb solo zakłada, że portfel korzysta z lokalnego noda.", + "miningPoolHint": "Domyślny pool dla Peya jest już wpisany i można go tu zmienić.", + "miningApiPortLabel": "Port API XMRig", + "miningResetDraftAction": "Przywróć szkic", + "miningConfigInvalid": "Podaj poprawny host poola i porty.", + "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", + "miningHashrate10sLabel": "Hashrate (10s)", + "miningHashrate1mLabel": "Hashrate (1m)", + "miningJobsLabel": "Joby", + "miningAcceptedSharesLabel": "Zaakceptowane udziały", + "miningRejectedSharesLabel": "Odrzucone udziały", + "miningBundledTitle": "Bundlowany miner", + "miningBundledEnabled": "Ten build zawiera bundlowaną binarkę XMRig.", + "miningBundledDisabled": "Ten wariant releasu nie zawiera bundlowanego minera.", + "miningBundledBody": "Warianty mining bundle'ują XMRig z dedykowanego repo Peya xmrig. Wariant lite całkowicie ukrywa ten tab.", "miningStartMiningEnabled": "Włączony ({threads} wątków)", "@miningStartMiningEnabled": { "placeholders": { diff --git a/lib/state/app_config_controller.dart b/lib/state/app_config_controller.dart index be21591..4367cf6 100644 --- a/lib/state/app_config_controller.dart +++ b/lib/state/app_config_controller.dart @@ -46,6 +46,48 @@ class AppConfigController extends StateNotifier { await updateConfig(state.copyWith(localNodeArgs: args)); } + Future setMiningConfig(MiningConfig config) async { + await updateConfig(state.copyWith(miningConfig: config)); + } + + Future setMiningMode(MiningMode mode) async { + await updateConfig( + state.copyWith(miningConfig: state.miningConfig.copyWith(mode: mode)), + ); + } + + Future setMiningCpuThreads(int value) async { + await updateConfig( + state.copyWith( + miningConfig: state.miningConfig.copyWith(cpuThreads: value), + ), + ); + } + + Future setMiningPoolHost(String value) async { + await updateConfig( + state.copyWith( + miningConfig: state.miningConfig.copyWith(poolHost: value), + ), + ); + } + + Future setMiningPoolPort(int value) async { + await updateConfig( + state.copyWith( + miningConfig: state.miningConfig.copyWith(poolPort: value), + ), + ); + } + + Future setMiningApiPort(int value) async { + await updateConfig( + state.copyWith( + miningConfig: state.miningConfig.copyWith(apiPort: value), + ), + ); + } + Future setP2poolStratum(String value) async { await updateConfig(state.copyWith(p2poolStratum: value)); } diff --git a/lib/ui/screens/home_shell.dart b/lib/ui/screens/home_shell.dart index 709c7e9..6ba8e44 100644 --- a/lib/ui/screens/home_shell.dart +++ b/lib/ui/screens/home_shell.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app_features.dart'; import '../l10n/app_localizations_ext.dart'; import '../widgets/status_bar.dart'; import 'account_screen.dart'; @@ -25,16 +26,54 @@ class _HomeShellState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = context.l10n; - final screens = [ - const AccountScreen(), - const SendScreen(), - const ReceiveScreen(), - const StakingScreen(), - const MiningScreen(), - const TransactionsScreen(), - const SettingsScreen(), + final items = <_NavItem>[ + _NavItem( + icon: const Icon(Symbols.account_box), + label: l10n.accountTitle, + screen: const AccountScreen(), + ), + _NavItem( + icon: const Icon(Symbols.call_made), + label: l10n.sendTitle, + screen: const SendScreen(), + ), + _NavItem( + icon: const Icon(Symbols.call_received), + label: l10n.receiveTitle, + screen: const ReceiveScreen(), + ), + _NavItem( + icon: const Icon(Symbols.chart_data), + label: l10n.stakingTitle, + screen: const StakingScreen(), + ), + if (AppFeatures.enableMining) + _NavItem( + icon: const Icon(Symbols.memory), + label: l10n.miningTitle, + screen: const MiningScreen(), + ), + _NavItem( + icon: const Icon(Symbols.list_alt), + label: l10n.transactionsTitle, + screen: const TransactionsScreen(), + ), + _NavItem( + icon: const Icon(Symbols.settings), + label: l10n.settingsTitle, + screen: const SettingsScreen(), + ), ]; + final selectedIndex = _selectedIndex.clamp(0, items.length - 1); + if (selectedIndex != _selectedIndex) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() => _selectedIndex = selectedIndex); + } + }); + } + return Scaffold( body: Row( children: [ @@ -42,7 +81,7 @@ class _HomeShellState extends ConsumerState { minWidth: 88, minExtendedWidth: 232, extended: true, - selectedIndex: _selectedIndex, + selectedIndex: selectedIndex, onDestinationSelected: (index) => setState(() => _selectedIndex = index), labelType: NavigationRailLabelType.none, @@ -50,42 +89,39 @@ class _HomeShellState extends ConsumerState { padding: EdgeInsets.fromLTRB(18, 18, 18, 10), child: _BrandHeader(), ), - destinations: [ - NavigationRailDestination( - icon: const Icon(Symbols.account_box), - label: Text(l10n.accountTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.call_made), - label: Text(l10n.sendTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.call_received), - label: Text(l10n.receiveTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.chart_data), - label: Text(l10n.stakingTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.memory), - label: Text(l10n.miningTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.list_alt), - label: Text(l10n.transactionsTitle)), - NavigationRailDestination( - icon: const Icon(Symbols.settings), - label: Text(l10n.settingsTitle)), - ], + destinations: items + .map( + (item) => NavigationRailDestination( + icon: item.icon, + label: Text(item.label), + ), + ) + .toList(), trailing: const Padding( padding: EdgeInsets.all(12), child: StatusBar(), ), ), const VerticalDivider(width: 1), - Expanded(child: screens[_selectedIndex]), + Expanded(child: items[selectedIndex].screen), ], ), ); } } +class _NavItem { + const _NavItem({ + required this.icon, + required this.label, + required this.screen, + }); + + final Widget icon; + final String label; + final Widget screen; +} + class _BrandHeader extends StatelessWidget { const _BrandHeader(); diff --git a/lib/ui/screens/mining_screen.dart b/lib/ui/screens/mining_screen.dart index 29a6a8d..f96b366 100644 --- a/lib/ui/screens/mining_screen.dart +++ b/lib/ui/screens/mining_screen.dart @@ -1,14 +1,535 @@ +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 '../../app_features.dart'; +import '../../domain/models.dart'; +import '../../state/providers.dart'; import '../l10n/app_localizations_ext.dart'; -import 'placeholder_screen.dart'; -class MiningScreen extends StatelessWidget { +class MiningScreen extends ConsumerStatefulWidget { const MiningScreen({super.key}); + @override + ConsumerState createState() => _MiningScreenState(); +} + +class _MiningScreenState extends ConsumerState { + late MiningMode _mode; + late int _cpuThreads; + late final TextEditingController _poolHostController; + late final TextEditingController _poolPortController; + late final TextEditingController _apiPortController; + bool _dirty = false; + + @override + void initState() { + super.initState(); + final config = ref.read(appConfigControllerProvider).miningConfig; + _mode = config.mode; + _cpuThreads = config.cpuThreads; + _poolHostController = TextEditingController(text: config.poolHost); + _poolPortController = TextEditingController(text: config.poolPort.toString()); + _apiPortController = TextEditingController(text: config.apiPort.toString()); + } + + @override + void dispose() { + _poolHostController.dispose(); + _poolPortController.dispose(); + _apiPortController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; - return PlaceholderScreen(title: l10n.miningTitle); + final theme = Theme.of(context); + final config = ref.watch(appConfigControllerProvider); + final walletState = ref.watch(walletControllerProvider); + final maxThreads = math.max(1, math.min(Platform.numberOfProcessors, 64)); + final poolHost = _poolHostController.text.trim(); + final poolPort = int.tryParse(_poolPortController.text.trim()); + final apiPort = int.tryParse(_apiPortController.text.trim()); + final canSave = poolHost.isNotEmpty && + poolPort != null && + poolPort > 0 && + apiPort != null && + apiPort > 0; + + 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), + LayoutBuilder( + builder: (context, constraints) { + final wide = constraints.maxWidth >= 920; + return Flex( + direction: wide ? Axis.horizontal : Axis.vertical, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: wide ? 3 : 0, + child: Column( + children: [ + _SectionCard( + title: l10n.miningSourceTitle, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DropdownButtonFormField( + value: _mode, + 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; + }); + }, + ), + const SizedBox(height: 16), + if (_mode == MiningMode.solo) ...[ + _InfoTile( + label: l10n.miningTargetLabel, + value: l10n.miningSoloTargetValue, + icon: Symbols.lan, + ), + const SizedBox(height: 12), + Text( + config.nodeConfig.mode == NodeMode.local + ? l10n.miningSoloHint + : l10n.miningSoloNeedsLocalNode, + style: theme.textTheme.bodyMedium?.copyWith( + color: config.nodeConfig.mode == NodeMode.local + ? theme.colorScheme.onSurfaceVariant + : theme.colorScheme.error, + ), + ), + ] else ...[ + TextFormField( + controller: _poolHostController, + decoration: InputDecoration( + labelText: l10n.hostLabel, + ), + onChanged: (_) => setState(() => _dirty = true), + ), + const SizedBox(height: 16), + TextFormField( + controller: _poolPortController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: l10n.portLabel, + ), + onChanged: (_) => setState(() => _dirty = true), + ), + const SizedBox(height: 12), + Text( + l10n.miningPoolHint, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 20), + DropdownButtonFormField( + value: _cpuThreads.clamp(1, maxThreads), + decoration: InputDecoration( + labelText: l10n.miningThreadsLabel, + ), + items: [ + for (var value = 1; value <= maxThreads; value++) + DropdownMenuItem( + value: value, + child: Text(value.toString()), + ), + ], + onChanged: (value) { + if (value == null) { + return; + } + setState(() { + _cpuThreads = value; + _dirty = true; + }); + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _apiPortController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: l10n.miningApiPortLabel, + ), + onChanged: (_) => setState(() => _dirty = true), + ), + const SizedBox(height: 16), + Row( + children: [ + FilledButton.icon( + onPressed: canSave ? _saveConfig : null, + icon: const Icon(Symbols.save), + label: Text(l10n.saveAction), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: _dirty ? _resetDraft : null, + icon: const Icon(Symbols.undo), + label: Text(l10n.miningResetDraftAction), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + _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), + ), + ], + ), + const SizedBox(height: 12), + Text( + l10n.miningBackendPending, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(width: wide ? 16 : 0, height: wide ? 0 : 16), + Expanded( + flex: wide ? 2 : 0, + child: Column( + children: [ + _SectionCard( + title: l10n.miningStatsTitle, + child: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _StatTile( + label: l10n.transactionStatusLabel, + value: l10n.comingSoon, + ), + _StatTile( + label: l10n.miningHashrate10sLabel, + value: l10n.miningDataUnknownValue, + ), + _StatTile( + label: l10n.miningHashrate1mLabel, + value: l10n.miningDataUnknownValue, + ), + _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, + ), + ], + ), + ), + 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.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.miningBundledTitle, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppFeatures.isMiningFlavor + ? l10n.miningBundledEnabled + : l10n.miningBundledDisabled, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Text( + l10n.miningBundledBody, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + } + + void _resetDraft() { + final config = ref.read(appConfigControllerProvider).miningConfig; + setState(() { + _mode = config.mode; + _cpuThreads = config.cpuThreads; + _poolHostController.text = config.poolHost; + _poolPortController.text = config.poolPort.toString(); + _apiPortController.text = config.apiPort.toString(); + _dirty = false; + }); + } + + Future _saveConfig() async { + final l10n = context.l10n; + final port = int.tryParse(_poolPortController.text.trim()); + final apiPort = int.tryParse(_apiPortController.text.trim()); + if (_poolHostController.text.trim().isEmpty || + port == null || + port <= 0 || + apiPort == null || + apiPort <= 0) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.miningConfigInvalid)), + ); + return; + } + + final next = MiningConfig( + mode: _mode, + cpuThreads: _cpuThreads, + poolHost: _poolHostController.text.trim(), + poolPort: port, + apiPort: apiPort, + ); + await ref.read(appConfigControllerProvider.notifier).setMiningConfig(next); + if (!mounted) { + return; + } + setState(() => _dirty = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.miningSettingsSaved)), + ); + } +} + +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 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, + ), + ], + ), + ), + ], + ); } } diff --git a/scripts/ci/stage_release_runtime.py b/scripts/ci/stage_release_runtime.py index 20cdb9f..2ec549d 100644 --- a/scripts/ci/stage_release_runtime.py +++ b/scripts/ci/stage_release_runtime.py @@ -117,7 +117,14 @@ def extract_member_from_zip(url: str, member_name: str, destination: Path, token write_bytes(destination, archive.read(member), executable=True) -def stage_linux(base_url: str, monero_c_tag: str, peya_tag: str, token: str | None, root: Path) -> None: +def stage_linux( + base_url: str, + monero_c_tag: str, + peya_tag: str, + xmrig_tag: str | None, + token: str | None, + root: Path, +) -> None: log("Staging Linux runtime") monero_release = release_metadata(base_url, "tiamak/monero_c", monero_c_tag, token) peya_release = release_metadata(base_url, "tiamak/Peya", peya_tag, token) @@ -135,10 +142,25 @@ def stage_linux(base_url: str, monero_c_tag: str, peya_tag: str, token: str | No peya_url = asset_download_url(base_url, peya_release, peya_asset, token) extract_member_from_tar(peya_url, "peyad", daemon_dir / "peyad", token) + if xmrig_tag: + miner_dir = root / "external" / "miner" + ensure_clean_dir(miner_dir) + xmrig_release = release_metadata(base_url, "tiamak/xmrig", xmrig_tag, token) + xmrig_asset = f"xmrig-peya-{xmrig_release['tag_name']}-linux-x86_64.tar.gz" + xmrig_url = asset_download_url(base_url, xmrig_release, xmrig_asset, token) + extract_member_from_tar(xmrig_url, "xmrig", miner_dir / "xmrig", token) + print(f"Staged Linux runtime from monero_c {monero_release['tag_name']} and Peya {peya_release['tag_name']}") -def stage_windows(base_url: str, monero_c_tag: str, peya_tag: str, token: str | None, root: Path) -> None: +def stage_windows( + base_url: str, + monero_c_tag: str, + peya_tag: str, + xmrig_tag: str | None, + token: str | None, + root: Path, +) -> None: log("Staging Windows runtime") monero_release = release_metadata(base_url, "tiamak/monero_c", monero_c_tag, token) peya_release = release_metadata(base_url, "tiamak/Peya", peya_tag, token) @@ -161,6 +183,14 @@ def stage_windows(base_url: str, monero_c_tag: str, peya_tag: str, token: str | peya_url = asset_download_url(base_url, peya_release, peya_asset, token) extract_member_from_zip(peya_url, "peyad.exe", daemon_dir / "peyad.exe", token) + if xmrig_tag: + miner_dir = root / "external" / "miner" + ensure_clean_dir(miner_dir) + xmrig_release = release_metadata(base_url, "tiamak/xmrig", xmrig_tag, token) + xmrig_asset = f"xmrig-peya-{xmrig_release['tag_name']}-windows-x86_64.zip" + xmrig_url = asset_download_url(base_url, xmrig_release, xmrig_asset, token) + extract_member_from_zip(xmrig_url, "xmrig.exe", miner_dir / "xmrig.exe", token) + print(f"Staged Windows runtime from monero_c {monero_release['tag_name']} and Peya {peya_release['tag_name']}") @@ -169,6 +199,7 @@ def main() -> int: parser.add_argument("--platform", choices=("linux", "windows"), required=True) parser.add_argument("--monero-c-tag", default="latest") parser.add_argument("--peya-tag", default="latest") + parser.add_argument("--xmrig-tag") parser.add_argument("--gitea-base-url", default=os.environ.get("GITEA_BASE_URL", DEFAULT_GITEA_BASE)) parser.add_argument("--repo-root", default=".") args = parser.parse_args() @@ -178,9 +209,23 @@ def main() -> int: try: if args.platform == "linux": - stage_linux(args.gitea_base_url, args.monero_c_tag, args.peya_tag, token, root) + stage_linux( + args.gitea_base_url, + args.monero_c_tag, + args.peya_tag, + args.xmrig_tag, + token, + root, + ) else: - stage_windows(args.gitea_base_url, args.monero_c_tag, args.peya_tag, token, root) + stage_windows( + args.gitea_base_url, + args.monero_c_tag, + args.peya_tag, + args.xmrig_tag, + token, + root, + ) except Exception as exc: log(f"stage_release_runtime.py failed: {exc}") raise