├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── .hooks └── pre-commit ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates └── core │ ├── Cargo.toml │ └── src │ ├── backup.rs │ ├── build │ └── build.rs │ ├── config │ ├── mod.rs │ └── wallet.rs │ ├── error.rs │ ├── fs │ ├── backup.rs │ ├── mod.rs │ ├── save.rs │ └── theme.rs │ ├── lib.rs │ ├── logger.rs │ ├── network.rs │ ├── node │ ├── mod.rs │ └── subscriber.rs │ ├── theme │ ├── application.rs │ ├── button.rs │ ├── card.rs │ ├── checkbox.rs │ ├── container.rs │ ├── mod.rs │ ├── modal.rs │ ├── picklist.rs │ ├── radio.rs │ ├── scrollable.rs │ ├── table_header.rs │ ├── table_row.rs │ ├── text.rs │ ├── text_editor.rs │ └── text_input.rs │ ├── utility.rs │ └── wallet │ └── mod.rs ├── fonts ├── notosans-bold.ttf └── notosans-regular.ttf ├── locale ├── de.json └── en.json ├── resources ├── linux │ └── grin-gui.desktop ├── logo │ ├── 1024x1024 │ │ ├── grin.png │ │ └── grin_macos.png │ ├── 128x128 │ │ └── grin.png │ ├── 16x16 │ │ └── grin.png │ ├── 24x24 │ │ └── grin.png │ ├── 256x256 │ │ └── grin.png │ ├── 32x32 │ │ └── grin.png │ ├── 48x48 │ │ └── grin.png │ ├── 512x512 │ │ └── grin.png │ └── 64x64 │ │ └── grin.png ├── osx │ └── GrinGUI.app │ │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ └── grin-gui.icns └── windows │ ├── grin.ico │ └── res.rc ├── rustfmt.toml ├── src ├── cli.rs ├── gui │ ├── element │ │ ├── about.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── modal.rs │ │ ├── node │ │ │ ├── embedded │ │ │ │ ├── mod.rs │ │ │ │ └── summary.rs │ │ │ └── mod.rs │ │ ├── settings │ │ │ ├── general.rs │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ └── wallet.rs │ │ └── wallet │ │ │ ├── mod.rs │ │ │ ├── operation │ │ │ ├── action_menu.rs │ │ │ ├── apply_tx.rs │ │ │ ├── apply_tx_confirm.rs │ │ │ ├── chart.rs │ │ │ ├── create_tx.rs │ │ │ ├── create_tx_contracts.rs │ │ │ ├── home.rs │ │ │ ├── mod.rs │ │ │ ├── open.rs │ │ │ ├── show_slatepack.rs │ │ │ ├── tx_detail.rs │ │ │ ├── tx_done.rs │ │ │ ├── tx_list.rs │ │ │ ├── tx_list_display.rs │ │ │ └── tx_proof.rs │ │ │ └── setup │ │ │ ├── init.rs │ │ │ ├── mod.rs │ │ │ ├── wallet_import.rs │ │ │ ├── wallet_import_success.rs │ │ │ ├── wallet_list.rs │ │ │ ├── wallet_setup.rs │ │ │ └── wallet_success.rs │ ├── mod.rs │ ├── style.rs │ ├── time.rs │ └── update.rs ├── localization.rs ├── main.rs ├── process.rs └── tray │ ├── autostart.rs │ └── mod.rs └── wix ├── License.rtf └── main.wxs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version of Grin-gui [e.g. v0.1.0-alpha.4 or a commit hash of the build] 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | linux-release: 10 | name: Linux Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Build 15 | run: cargo build --release 16 | - name: Archive 17 | working-directory: target/release 18 | run: tar -czvf grin-gui-${{ github.ref_name }}-linux-x86_64.tar.gz grin-gui 19 | - name: Create Checksum 20 | working-directory: target/release 21 | run: openssl sha256 grin-gui-${{ github.ref_name }}-linux-x86_64.tar.gz > grin-gui-${{ github.ref_name }}-linux-x86_64-sha256sum.txt 22 | - name: Release 23 | uses: softprops/action-gh-release@v1 24 | with: 25 | generate_release_notes: true 26 | files: | 27 | target/release/grin-gui-${{ github.ref_name }}-linux-x86_64.tar.gz 28 | target/release/grin-gui-${{ github.ref_name }}-linux-x86_64-sha256sum.txt 29 | 30 | macos-release: 31 | name: macOS Release 32 | runs-on: macos-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | - name: Build 37 | run: cargo build --release 38 | - name: Archive 39 | working-directory: target/release 40 | run: tar -czvf grin-gui-${{ github.ref_name }}-macos-x86_64.tar.gz grin-gui 41 | - name: Create Checksum 42 | working-directory: target/release 43 | run: openssl sha256 grin-gui-${{ github.ref_name }}-macos-x86_64.tar.gz > grin-gui-${{ github.ref_name }}-macos-x86_64-sha256sum.txt 44 | - name: Release 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | files: | 48 | target/release/grin-gui-${{ github.ref_name }}-macos-x86_64.tar.gz 49 | target/release/grin-gui-${{ github.ref_name }}-macos-x86_64-sha256sum.txt 50 | 51 | windows-release: 52 | name: Windows Release 53 | runs-on: windows-2019 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v3 57 | - name: Rust version 58 | run: rustup --version 59 | - name: Build 60 | run: cargo build --release 61 | - name: Build 62 | run: cargo build 63 | - name: Archive release 64 | uses: vimtor/action-zip@v1 65 | with: 66 | files: target/release/grin-gui.exe 67 | dest: target/release/grin-gui-${{ github.ref_name }}-win-x86_64.zip 68 | - name: Archive debug 69 | uses: vimtor/action-zip@v1 70 | with: 71 | files: target/debug/grin-gui.exe 72 | dest: target/debug/grin-gui-${{ github.ref_name }}-win-x86_64-debug.zip 73 | - name: Create Checksum release 74 | working-directory: target/release 75 | shell: pwsh 76 | run: get-filehash -algorithm sha256 grin-gui-${{ github.ref_name }}-win-x86_64.zip | Format-List |  Out-String | ForEach-Object { $_.Trim() } > grin-gui-${{ github.ref_name }}-win-x86_64-sha256sum.txt 77 | - name: Create Checksum debug 78 | working-directory: target/debug 79 | shell: pwsh 80 | run: get-filehash -algorithm sha256 grin-gui-${{ github.ref_name }}-win-x86_64-debug.zip | Format-List |  Out-String | ForEach-Object { $_.Trim() } > grin-gui-${{ github.ref_name }}-win-x86_64-debug-sha256sum.txt 81 | - name: Install cargo-wix 82 | run: cargo install cargo-wix 83 | - name: Run cargo-wix 84 | run: cargo wix -p grin-gui -o ./target/wix/grin-gui-${{ github.ref_name }}-win-x86_64.msi 85 | - name: Create Checksum for MSI 86 | working-directory: target/wix 87 | shell: pwsh 88 | run: get-filehash -algorithm sha256 grin-gui-${{ github.ref_name }}-win-x86_64.msi | Format-List |  Out-String | ForEach-Object { $_.Trim() } > grin-gui-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt 89 | - name: Release 90 | uses: softprops/action-gh-release@v1 91 | with: 92 | files: | 93 | target/debug/grin-gui-${{ github.ref_name }}-win-x86_64-debug.zip 94 | target/debug/grin-gui-${{ github.ref_name }}-win-x86_64-debug-sha256sum.txt 95 | target/release/grin-gui-${{ github.ref_name }}-win-x86_64.zip 96 | target/release/grin-gui-${{ github.ref_name }}-win-x86_64-sha256sum.txt 97 | target/wix/grin-gui-${{ github.ref_name }}-win-x86_64.msi 98 | target/wix/grin-gui-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | linux-tests: 6 | name: Linux Tests 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | job_args: [crates/core, .] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Test ${{ matrix.job_args }} 14 | working-directory: ${{ matrix.job_args }} 15 | run: cargo test --release 16 | 17 | macos-tests: 18 | name: macOS Tests 19 | runs-on: macos-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | - name: Tests 24 | run: cargo test --release --all 25 | 26 | windows-tests: 27 | name: Windows Tests 28 | runs-on: windows-2019 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | - name: Tests 33 | run: cargo test --release --all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | */*/target 4 | -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash;#!C:/Program\ Files/Git/usr/bin/bash.exe 2 | 3 | # Copyright 2024 The Grin Developers 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | rustfmt --version &>/dev/null 18 | if [ $? != 0 ]; then 19 | printf "[pre_commit] \033[0;31merror\033[0m: \"rustfmt\" not available. \n" 20 | printf "[pre_commit] \033[0;31merror\033[0m: rustfmt can be installed via - \n" 21 | printf "[pre_commit] $ rustup component add rustfmt-preview \n" 22 | exit 1 23 | fi 24 | 25 | problem_files=() 26 | 27 | # first collect all the files that need reformatting 28 | for file in $(git diff --name-only --cached); do 29 | if [ ${file: -3} == ".rs" ]; then 30 | rustfmt --check $file &>/dev/null 31 | if [ $? != 0 ]; then 32 | problem_files+=($file) 33 | fi 34 | fi 35 | done 36 | 37 | if [ ${#problem_files[@]} == 0 ]; then 38 | # nothing to do 39 | printf "[pre_commit] rustfmt \033[0;32mok\033[0m \n" 40 | else 41 | # reformat the files that need it and re-stage them. 42 | printf "[pre_commit] the following files were rustfmt'd before commit: \n" 43 | for file in ${problem_files[@]}; do 44 | rustfmt $file 45 | git add $file 46 | printf "\033[0;32m $file\033[0m \n" 47 | done 48 | fi 49 | 50 | # flush stdout on windows so all output is visible 51 | if [ "$OS" == "Windows_NT" ]; then 52 | exec < /dev/tty 53 | fi 54 | 55 | exit 0 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grin-gui" 3 | version = "0.1.0-alpha.7" 4 | authors = ["Grin Developers "] 5 | description = "GUI wrapping grin and grin-wallet. Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." 6 | license = "Apache-2.0" 7 | repository = "https://github.com/mimblewimble/grin-gui" 8 | keywords = [ "crypto", "grin", "mimblewimble" ] 9 | readme = "README.md" 10 | edition = "2021" 11 | 12 | [features] 13 | default = ["wgpu"] 14 | wgpu = [ "iced_renderer/wgpu" ] 15 | no-self-update = ["grin-gui-core/no-self-update"] 16 | debug = ["iced/debug"] 17 | 18 | [dependencies] 19 | grin-gui-core = { version = "0.1.0-alpha.8", path = "crates/core", features = ["wgpu"]} 20 | 21 | iced = { version = "0.12", features = ["canvas", "tokio"] } 22 | iced_futures = { version = "0.12", features = ["async-std"] } 23 | iced_style = {version = "0.12"} 24 | iced_renderer = { version = "0.12" } 25 | iced_core = { version = "0.12" } 26 | #iced_aw = { path = "../iced_aw", default-features = false, features = ["card", "modal", "table"]} 27 | iced_aw = { git = "https://github.com/yeastplume/iced_aw.git", branch = "table_widget", default-features = false, features = ["card", "modal", "table"]} 28 | 29 | plotters-iced = "0.10.0" 30 | plotters="0.3" 31 | plotters-backend = "0.3" 32 | rand = "0.8.3" 33 | 34 | async-std = "1.6.2" 35 | isahc = { version = "0.9.6", features = ["json"] } 36 | image = "0.23.8" 37 | opener = "0.4.1" 38 | chrono = { version = "0.4.11", features = ["serde"] } 39 | log = "0.4" 40 | timeago = "0.2.1" 41 | isolang = "1.0.0" 42 | log-panics = { version = "2.0", features=['with-backtrace'] } 43 | structopt = "0.3" 44 | num-format = "0.4.0" 45 | futures = "0.3" 46 | version-compare = "0.0.11" 47 | open = "1" 48 | anyhow = "1.0" 49 | fuzzy-matcher = "0.3.7" 50 | json-gettext = "3.2.8" 51 | strfmt = "0.1.6" 52 | once_cell = "1.6.0" 53 | lazy_static = "1" 54 | serde = { version = "1", features=['derive'] } 55 | serde_json = "1" 56 | reqwest = { version = "0.11", features = ["json", "blocking"] } 57 | uuid = "0.8.2" 58 | 59 | 60 | [target.'cfg(target_os = "linux")'.dependencies] 61 | native-dialog = "0.5.5" 62 | 63 | [target.'cfg(not(target_os = "linux"))'.dependencies] 64 | native-dialog = "0.6.3" 65 | rfd = "0.4.0" 66 | 67 | [target.'cfg(windows)'.dependencies] 68 | winapi = "0.3.9" 69 | 70 | [build-dependencies] 71 | embed-resource = "1.3.3" 72 | 73 | [workspace] 74 | members = [ 75 | ".", 76 | "crates/core", 77 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grin GUI 2 | This is a very Work-in-Progress implementation of the Grin Core Team's Integrated GUI for both Grin Wallet and Grin Node. 3 | 4 | # Goals 5 | 6 | The Grin team has spent countless hours making Grin's infrastructure extremely flexible, with multiple ways of running nodes and wallets and extensive developer APIs and documentation for both. 7 | 8 | This project aims to pull all of this work together to create a lightweight, flexible and user-friendly method of using Grin. This includes: 9 | 10 | * Presenting a completely working Single UI for both Grin Node and Grin Wallet. 11 | * Allowing all wallet transaction operations to be performed in a user-friendly and intuitive manner 12 | * Create and manage a Grin node in-application if desired, while also retaining the options to communicate with other configured or public nodes. 13 | * The ability to create, configure and manage multiple wallets and nodes including existing installations 14 | 15 | # Status 16 | 17 | **NOTHING WORKS AT PRESENT** 18 | 19 | * UI Framework [iced-rs](https://github.com/iced-rs/iced) has been selected. 20 | * Overall structure of project is in place based on [ajour](https://github.com/ajour/ajour) as a sample project 21 | * Some refactoring of project structure to better separate GUI elements and events 22 | * Theming, localization, UI scaling is in place 23 | * Windows systray functionality in place 24 | 25 | # Current Focus 26 | 27 | In contrast to most Grin development, Grin GUI is being developed on Windows, with Windows being the first-class citizen. MacOS and Linux will of course also be supported. 28 | 29 | Current work is: 30 | 31 | * Including grin wallet + API 32 | * First-time 'Out of Box' setup and creation of a grin wallet from the UI 33 | * Wallet + Node configuration options 34 | 35 | # Contributing 36 | 37 | Yes please! This is an excellent project for anyone wanting to get their feet wet with Grin development. Detailed knowledge of Grin's internals is not required, just a familiarity with Grin's APIs and a willingness to dive into [iced-rs](https://github.com/iced-rs/iced). 38 | 39 | See [Grin project contribution](https://github.com/mimblewimble/grin/blob/master/CONTRIBUTING.md) for general guidelines we'll eventually be using, however this project is still far too new for most of this to be relevant. 40 | 41 | # Building 42 | 43 | ## Prerequisites 44 | * rust: Install using rustup: https://rustup.rs 45 | * Rustc version >= 1.59 46 | * it is recommended to build using the latest version. 47 | * If rust is already installed, you can update to the latest version by running `rustup update` 48 | 49 | ### Windows 50 | * `llvm` must be installed 51 | 52 | ### Linux 53 | > For Debian-based distributions (Debian, Ubuntu, Mint, etc), all in one line (except Rust): 54 | 55 | * ``` sudo apt install build-essential cmake git libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev pkg-config libssl-dev llvm libfontconfig libfontconfig1-dev``` 56 | 57 | (instructions not yet complete) 58 | 59 | # Acknowledgement 60 | 61 | * Thanks to [iced-rs](https://github.com/iced-rs/iced) for a workable native Rust GUI 62 | * Thank you to [ajour](https://github.com/ajour/ajour) for a completely working, non-trivial and tested-in-the-wild iced-rs project to use as a base for development. 63 | 64 | # License 65 | 66 | GPL 3.0 (for the time being) 67 | 68 | Note this differs from the rest of the Grin codebase (which uses Apache 2) due to [ajour](https://github.com/ajour/ajour)'s licensing. Currently attempting to get copyright holder's permission to change this to Apache 2. 69 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grin-gui-core" 3 | description = "Core GUI library for Grin GUI" 4 | version = "0.1.0-alpha.8" 5 | authors = ["Yeastplume", "Casper Rogild Storm"] 6 | license = "GPL-3.0" 7 | homepage = "https://github.com/mimblewimble/grin-gui" 8 | repository = "https://github.com/mimblewimble/grin-gui" 9 | edition = "2018" 10 | build = "src/build/build.rs" 11 | 12 | [features] 13 | default = ["wgpu"] 14 | no-self-update = [] 15 | wgpu = ["iced_renderer/wgpu"] 16 | 17 | [build-dependencies] 18 | built = { version = "0.4", features = ["git2"] } 19 | 20 | [dependencies] 21 | 22 | ############ Release ################ 23 | ### Node 24 | #grin_config = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 25 | #grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 26 | #grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 27 | #grin_servers = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 28 | #grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 29 | #grin_chain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } 30 | 31 | grin_config = { git = "https://github.com/mimblewimble/grin", branch = "master"} 32 | grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master"} 33 | grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master"} 34 | grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "master"} 35 | grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master"} 36 | grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master"} 37 | 38 | ### Wallet 39 | grin_wallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 40 | grin_wallet_config = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 41 | grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 42 | grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 43 | grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 44 | grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 45 | grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 46 | 47 | ############ Local testing ################ 48 | ### Node 49 | # grin_config = { path = "../../../grin/config" } 50 | # grin_core = { path = "../../../grin/core" } 51 | # grin_util = { path = "../../../grin/util" } 52 | # grin_servers = { path = "../../../grin/servers" } 53 | # grin_keychain = { path = "../../../grin/keychain" } 54 | # grin_chain = { path = "../../../grin/chain" } 55 | 56 | ### Wallet 57 | #grin_wallet = { path = "../../../grin-wallet"} 58 | #grin_wallet_config = { path = "../../../grin-wallet/config"} 59 | #grin_wallet_util = { path = "../../../grin-wallet/util"} 60 | #grin_wallet_controller = { path = "../../../grin-wallet/controller"} 61 | #grin_wallet_api = { path = "../../../grin-wallet/api"} 62 | #grin_wallet_impls = { path = "../../../grin-wallet/impls"} 63 | #grin_wallet_libwallet = { path = "../../../grin-wallet/libwallet"} 64 | 65 | regex = "1.4.3" 66 | fancy-regex = "0.5.0" # Regex with backtracking 67 | async-std = { version = "1.9.0", features = ["unstable"] } 68 | dirs-next = "2.0.0" 69 | serde = { version = "1", features = ['derive'] } 70 | serde_yaml = "0.8.17" 71 | serde_json = "1.0.62" 72 | serde_urlencoded = "0.7" 73 | isahc = { version = "1.1.0", features = ["json"] } 74 | zip = "0.5.10" 75 | glob = "0.3.0" 76 | once_cell = "1.6.0" 77 | chrono = { version = "0.4.11", features = ['serde'] } 78 | log = "0.4" 79 | walkdir = "2.3" 80 | retry = "1.2" 81 | thiserror = "1.0" 82 | path-slash = "0.1.4" 83 | tar = "0.4.33" 84 | zstd = { version = "0.6.1", features = ["zstdmt"] } 85 | num_cpus = "1.13.0" 86 | dirs = "2.0" 87 | futures = "0.3" 88 | parking_lot = "0.10" 89 | log4rs = { version = "0.12", features = [ 90 | "rolling_file_appender", 91 | "compound_policy", 92 | "size_trigger", 93 | "fixed_window_roller", 94 | ] } 95 | backtrace = "0.3" 96 | lazy_static = "1" 97 | uuid = "0.8.2" 98 | 99 | iced = { version = "0.12", features = ["advanced", "tokio"] } 100 | iced_futures = { version = "0.12", features = ["async-std"] } 101 | iced_core = { version = "0.12" } 102 | iced_style = "0.12" 103 | iced_graphics = { version = "0.12" } 104 | iced_renderer = { version = "0.12", features = ["wgpu"] } 105 | #iced_aw = { path = "../../../iced_aw", default-features = false, features = ["card", "modal", "table"]} 106 | iced_aw = { git = "https://github.com/yeastplume/iced_aw.git", branch = "table_widget", default-features = false, features = ["card", "modal", "table"]} 107 | 108 | [dev-dependencies] 109 | tempfile = "3.2.0" 110 | 111 | [target.'cfg(target_os = "macos")'.dependencies] 112 | flate2 = "1.0" 113 | tar = "0.4" 114 | -------------------------------------------------------------------------------- /crates/core/src/backup.rs: -------------------------------------------------------------------------------- 1 | use crate::error::FilesystemError; 2 | use crate::fs::backup::{Backup, ZipBackup, ZstdBackup}; 3 | 4 | use chrono::{Local, NaiveDateTime}; 5 | use std::convert::TryFrom; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use std::fmt::Display; 9 | use std::str::FromStr; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, PartialOrd, Ord, Deserialize)] 14 | pub enum CompressionFormat { 15 | Zip, 16 | Zstd, 17 | } 18 | 19 | impl CompressionFormat { 20 | pub const ALL: [CompressionFormat; 2] = [CompressionFormat::Zip, CompressionFormat::Zstd]; 21 | 22 | pub(crate) const fn file_ext(&self) -> &'static str { 23 | match self { 24 | CompressionFormat::Zip => "zip", 25 | CompressionFormat::Zstd => "tar.zst", 26 | } 27 | } 28 | } 29 | 30 | impl Default for CompressionFormat { 31 | fn default() -> CompressionFormat { 32 | CompressionFormat::Zip 33 | } 34 | } 35 | 36 | impl Display for CompressionFormat { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | match self { 39 | CompressionFormat::Zip => f.write_str("Zip"), 40 | CompressionFormat::Zstd => f.write_str("Zstd"), 41 | } 42 | } 43 | } 44 | 45 | impl FromStr for CompressionFormat { 46 | type Err = &'static str; 47 | 48 | fn from_str(s: &str) -> Result { 49 | match s { 50 | "zip" | "Zip" => Ok(CompressionFormat::Zip), 51 | "zstd" | "Zstd" => Ok(CompressionFormat::Zstd), 52 | _ => Err("valid values are: zip, zstd"), 53 | } 54 | } 55 | } 56 | 57 | /// Creates a .zip archive from the list of source folders and 58 | /// saves it to the dest folder. 59 | pub async fn backup_folders( 60 | src_folders: Vec, 61 | mut dest: PathBuf, 62 | compression: CompressionFormat, 63 | zstd_level: i32, 64 | ) -> Result { 65 | let now = Local::now(); 66 | 67 | dest.push(format!( 68 | "grin_gui_backup_{}.{}", 69 | now.format("%Y-%m-%d_%H-%M-%S"), 70 | compression.file_ext(), 71 | )); 72 | 73 | match compression { 74 | CompressionFormat::Zip => ZipBackup::new(src_folders, &dest).backup()?, 75 | CompressionFormat::Zstd => ZstdBackup::new(src_folders, &dest, zstd_level).backup()?, 76 | } 77 | 78 | // Won't fail since we pass it the correct format 79 | let as_of = Archive::try_from(dest).unwrap().as_of; 80 | 81 | Ok(as_of) 82 | } 83 | 84 | /// Finds the latest archive in the supplied backup folder and returns 85 | /// the datetime it was saved 86 | pub async fn latest_backup(backup_dir: PathBuf) -> Option { 87 | let zip_pattern = format!("{}/grin_gui_backup_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]-[0-9][0-9]-[0-9][0-9].zip", backup_dir.display()); 88 | let zstd_pattern = format!("{}/grin_gui_backup_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]-[0-9][0-9]-[0-9][0-9].tar.zst", backup_dir.display()); 89 | 90 | let mut backups = vec![]; 91 | 92 | for path in glob::glob(&zip_pattern) 93 | .unwrap() 94 | .chain(glob::glob(&zstd_pattern).unwrap()) 95 | .flatten() 96 | { 97 | if let Ok(archive) = Archive::try_from(path) { 98 | backups.push(archive.as_of); 99 | } 100 | } 101 | 102 | // Apparently NaiveDateTime sorts in Desc order by default, no need to reverse 103 | backups.sort(); 104 | backups.pop() 105 | } 106 | 107 | /// Specifies a folder that we want backed up. `prefix` will get stripped out of 108 | /// the path of each entry in the archive. 109 | pub struct BackupFolder { 110 | pub path: PathBuf, 111 | pub prefix: PathBuf, 112 | } 113 | 114 | impl BackupFolder { 115 | pub fn new(path: impl AsRef, prefix: impl AsRef) -> BackupFolder { 116 | BackupFolder { 117 | path: path.as_ref().to_owned(), 118 | prefix: prefix.as_ref().to_owned(), 119 | } 120 | } 121 | } 122 | 123 | /// Metadata for our archive saved on the filesystem. Converted from a `PathBuf` with 124 | /// the correct naming convention 125 | struct Archive { 126 | pub as_of: NaiveDateTime, 127 | } 128 | 129 | impl TryFrom for Archive { 130 | type Error = chrono::ParseError; 131 | 132 | fn try_from(path: PathBuf) -> Result { 133 | let mut file_stem = path.file_stem().unwrap().to_str().unwrap(); 134 | 135 | // in the case of "file.tar.zst" path.file_stem() will return "file.tar", we still need to 136 | // drop the extension 137 | if let Some(i) = file_stem.find('.') { 138 | file_stem = file_stem.split_at(i).0; 139 | } 140 | 141 | let date_str = format!( 142 | "{} {}", 143 | file_stem.split('_').nth(2).unwrap_or_default(), 144 | file_stem.split('_').nth(3).unwrap_or_default() 145 | ); 146 | 147 | let as_of = NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%d %H-%M-%S")?; 148 | 149 | Ok(Archive { as_of }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /crates/core/src/build/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Grin Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Build hooks to spit out version+build time info 16 | 17 | use std::env; 18 | use std::path::{Path, PathBuf}; 19 | use std::process::Command; 20 | 21 | fn main() { 22 | // Setting up git hooks in the project: rustfmt and so on. 23 | let git_hooks = format!( 24 | "git config core.hooksPath {}", 25 | PathBuf::from("./.hooks").to_str().unwrap() 26 | ); 27 | 28 | if cfg!(target_os = "windows") { 29 | Command::new("cmd") 30 | .args(&["/C", &git_hooks]) 31 | .output() 32 | .expect("failed to execute git config for hooks"); 33 | } else { 34 | Command::new("sh") 35 | .args(&["-c", &git_hooks]) 36 | .output() 37 | .expect("failed to execute git config for hooks"); 38 | } 39 | 40 | // build and versioning information 41 | let mut opts = built::Options::default(); 42 | opts.set_dependencies(true); 43 | let out_dir_path = format!("{}{}", env::var("OUT_DIR").unwrap(), "/built.rs"); 44 | // don't fail the build if something's missing, may just be cargo release 45 | let _ = built::write_built_file_with_opts( 46 | &opts, 47 | Path::new(env!("CARGO_MANIFEST_DIR")), 48 | Path::new(&out_dir_path), 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /crates/core/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::backup::CompressionFormat; 2 | use crate::error::FilesystemError; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::{self, Display, Formatter}; 5 | use std::path::PathBuf; 6 | 7 | mod wallet; 8 | 9 | use crate::fs::PersistentData; 10 | 11 | pub use crate::config::wallet::Wallet; 12 | 13 | /// Config struct. 14 | #[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)] 15 | pub struct Config { 16 | /// Configured wallet definitions 17 | #[serde(default)] 18 | pub wallets: Vec, 19 | 20 | /// Current wallet 21 | pub current_wallet_index: Option, 22 | 23 | /// MWMixnet Keys 24 | pub mixnet_keys: Option>, 25 | 26 | /// Current theme of GUI 27 | pub theme: Option, 28 | 29 | /// User preferred currency 30 | pub currency: Currency, 31 | 32 | #[serde(default)] 33 | pub tx_column_config: ColumnConfig, 34 | 35 | pub window_size: Option<(u32, u32)>, 36 | 37 | pub scale: Option, 38 | 39 | pub backup_directory: Option, 40 | 41 | #[serde(default)] 42 | pub self_update_channel: SelfUpdateChannel, 43 | 44 | #[serde(default = "default_true")] 45 | pub alternating_row_colors: bool, 46 | 47 | //TODO: These default values aren't working 48 | #[serde(default = "default_true")] 49 | pub is_keybindings_enabled: bool, 50 | 51 | #[serde(default)] 52 | pub language: Language, 53 | 54 | #[serde(default)] 55 | pub auto_update: bool, 56 | 57 | #[serde(default)] 58 | pub compression_format: CompressionFormat, 59 | 60 | #[serde(default)] 61 | pub zstd_compression_level: i32, 62 | 63 | #[serde(default)] 64 | #[cfg(target_os = "windows")] 65 | pub close_to_tray: bool, 66 | 67 | #[serde(default)] 68 | #[cfg(target_os = "windows")] 69 | pub autostart: bool, 70 | 71 | #[serde(default)] 72 | #[cfg(target_os = "windows")] 73 | pub start_closed_to_tray: bool, 74 | 75 | #[serde(default)] 76 | pub tx_method: TxMethod, 77 | } 78 | 79 | impl Config { 80 | pub fn add_wallet(&mut self, wallet: Wallet) -> usize { 81 | self.wallets.push(wallet); 82 | self.wallets.len() - 1 83 | } 84 | 85 | pub fn get_wallet_slatepack_dir(&self) -> Option { 86 | if let Some(i) = self.current_wallet_index.as_ref() { 87 | if let Some(ref tld) = self.wallets[*i].tld { 88 | let slate_dir = format!("{}/{}", tld.as_os_str().to_str().unwrap(), "slatepack"); 89 | let _ = std::fs::create_dir_all(slate_dir.clone()); 90 | Some(slate_dir) 91 | } else { 92 | None 93 | } 94 | } else { 95 | None 96 | } 97 | } 98 | } 99 | 100 | impl PersistentData for Config { 101 | fn relative_path() -> PathBuf { 102 | PathBuf::from("grin-gui.yml") 103 | } 104 | } 105 | 106 | #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] 107 | pub enum ColumnConfig { 108 | V1 { 109 | local_version_width: u16, 110 | remote_version_width: u16, 111 | status_width: u16, 112 | }, 113 | V2 { 114 | columns: Vec, 115 | }, 116 | V3 { 117 | my_addons_columns: Vec, 118 | catalog_columns: Vec, 119 | #[serde(default)] 120 | aura_columns: Vec, 121 | }, 122 | } 123 | 124 | #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] 125 | pub struct ColumnConfigV2 { 126 | pub key: String, 127 | pub width: Option, 128 | pub hidden: bool, 129 | } 130 | 131 | impl Default for ColumnConfig { 132 | fn default() -> Self { 133 | ColumnConfig::V1 { 134 | local_version_width: 150, 135 | remote_version_width: 150, 136 | status_width: 85, 137 | } 138 | } 139 | } 140 | 141 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 142 | pub enum SelfUpdateChannel { 143 | Stable, 144 | Beta, 145 | } 146 | 147 | impl SelfUpdateChannel { 148 | pub const fn all() -> [Self; 2] { 149 | [SelfUpdateChannel::Stable, SelfUpdateChannel::Beta] 150 | } 151 | } 152 | 153 | impl Default for SelfUpdateChannel { 154 | fn default() -> Self { 155 | SelfUpdateChannel::Stable 156 | } 157 | } 158 | 159 | impl Display for SelfUpdateChannel { 160 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 161 | let s = match self { 162 | SelfUpdateChannel::Stable => "Stable", 163 | SelfUpdateChannel::Beta => "Beta", 164 | }; 165 | 166 | write!(f, "{}", s) 167 | } 168 | } 169 | 170 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)] 171 | pub enum Language { 172 | English, 173 | German, 174 | } 175 | 176 | impl std::fmt::Display for Language { 177 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 178 | write!( 179 | f, 180 | "{}", 181 | match self { 182 | Language::English => "English", 183 | Language::German => "Deutsch", 184 | } 185 | ) 186 | } 187 | } 188 | 189 | impl Language { 190 | // Alphabetically sorted based on their local name (@see `impl Display`). 191 | pub const ALL: [Language; 2] = [Language::German, Language::English]; 192 | 193 | pub const fn language_code(self) -> &'static str { 194 | match self { 195 | Language::English => "en_US", 196 | Language::German => "de_DE", 197 | } 198 | } 199 | } 200 | 201 | impl Default for Language { 202 | fn default() -> Language { 203 | Language::English 204 | } 205 | } 206 | 207 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)] 208 | pub enum TxMethod { 209 | Legacy, 210 | Contracts, 211 | } 212 | 213 | impl TxMethod { 214 | // Alphabetically sorted based on their local name (@see `impl Display`). 215 | pub const ALL: [TxMethod; 2] = [TxMethod::Legacy, TxMethod::Contracts]; 216 | } 217 | 218 | impl std::fmt::Display for TxMethod { 219 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 220 | write!( 221 | f, 222 | "{}", 223 | match self { 224 | TxMethod::Legacy => "Legacy", 225 | TxMethod::Contracts => "Contracts", 226 | } 227 | ) 228 | } 229 | } 230 | 231 | impl Default for TxMethod { 232 | fn default() -> TxMethod { 233 | TxMethod::Legacy 234 | } 235 | } 236 | 237 | #[derive( 238 | Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord, 239 | )] 240 | pub enum Currency { 241 | #[default] 242 | GRIN, 243 | BTC, 244 | USD, 245 | } 246 | 247 | impl std::fmt::Display for Currency { 248 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 249 | write!( 250 | f, 251 | "{}", 252 | match self { 253 | Currency::BTC => "Bitcoin", 254 | Currency::USD => "US Dollar", 255 | Currency::GRIN => "Grin", 256 | } 257 | ) 258 | } 259 | } 260 | 261 | impl Currency { 262 | // Alphabetically sorted based on their local name (@see `impl Display`). 263 | pub const ALL: [Currency; 3] = [Currency::BTC, Currency::GRIN, Currency::USD]; 264 | 265 | pub fn shortname(&self) -> String { 266 | match *self { 267 | Currency::BTC => "btc".to_owned(), 268 | Currency::GRIN => "grin".to_owned(), 269 | Currency::USD => "usd".to_owned(), 270 | } 271 | } 272 | 273 | pub fn symbol(&self) -> String { 274 | match *self { 275 | Currency::BTC => "₿".to_owned(), 276 | Currency::GRIN => "".to_owned(), 277 | Currency::USD => "$".to_owned(), 278 | } 279 | } 280 | 281 | pub fn precision(&self) -> usize { 282 | match *self { 283 | Currency::BTC => 8, 284 | Currency::GRIN => 9, 285 | Currency::USD => 4, 286 | } 287 | } 288 | } 289 | 290 | /// Returns a Config. 291 | /// 292 | /// This functions handles the initialization of a Config. 293 | pub async fn load_config() -> Result { 294 | log::debug!("loading config"); 295 | 296 | Ok(Config::load_or_default()?) 297 | } 298 | 299 | const fn default_true() -> bool { 300 | true 301 | } 302 | 303 | #[cfg(test)] 304 | mod test { 305 | 306 | /// This method will take a relative path and make a case insentitive pattern 307 | // For some reason the case insensitive pattern doesn't work 308 | // unless we add an actual pattern symbol, hence the `?`. 309 | fn get_pattern_format(relative_path: &str) -> String { 310 | let splitted_string = relative_path.split('/'); 311 | let mut return_string: Vec = vec![]; 312 | for path in splitted_string { 313 | let mut to_lower_case = path.to_lowercase(); 314 | to_lower_case.replace_range(0..1, "?"); 315 | return_string.push(to_lower_case); 316 | } 317 | return_string.join("/") 318 | } 319 | 320 | #[test] 321 | fn test_get_format_interface_addons() { 322 | assert_eq!( 323 | get_pattern_format("Interface/Addons"), 324 | String::from("?nterface/?ddons") 325 | ); 326 | assert_eq!( 327 | get_pattern_format("iNtErFaCe/aDdoNs"), 328 | String::from("?nterface/?ddons") 329 | ); 330 | } 331 | 332 | #[test] 333 | fn test_get_format_wtf() { 334 | assert_eq!(get_pattern_format("WTF"), String::from("?tf")); 335 | assert_eq!(get_pattern_format("wTF"), String::from("?tf")); 336 | assert_eq!(get_pattern_format("Wtf"), String::from("?tf")); 337 | assert_eq!(get_pattern_format("wTf"), String::from("?tf")); 338 | } 339 | 340 | #[test] 341 | fn test_get_format_screenshots() { 342 | assert_eq!( 343 | get_pattern_format("Screenshots"), 344 | String::from("?creenshots") 345 | ); 346 | assert_eq!( 347 | get_pattern_format("sCREENSHOTS"), 348 | String::from("?creenshots") 349 | ); 350 | assert_eq!( 351 | get_pattern_format("ScreeNShots"), 352 | String::from("?creenshots") 353 | ); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /crates/core/src/config/wallet.rs: -------------------------------------------------------------------------------- 1 | use grin_core::global::ChainTypes; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::PathBuf; 4 | 5 | /// Struct for settings related to World of Warcraft. 6 | #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] 7 | #[serde(default)] 8 | pub struct Wallet { 9 | #[serde(default)] 10 | #[allow(deprecated)] 11 | /// Top-level directory. Should (but not always) contain grin_wallet.toml file 12 | pub tld: Option, 13 | /// Display name in wallet selection 14 | pub display_name: String, 15 | /// If true, override the grin_wallet.toml configured node and use the internal one 16 | pub use_embedded_node: bool, 17 | /// Chain type of wallet 18 | pub chain_type: ChainTypes, 19 | } 20 | 21 | impl Wallet { 22 | pub fn new(tld: Option, display_name: String, chain_type: ChainTypes) -> Self { 23 | Self { 24 | tld, 25 | display_name, 26 | use_embedded_node: true, 27 | chain_type, 28 | } 29 | } 30 | } 31 | 32 | impl Default for Wallet { 33 | fn default() -> Self { 34 | Wallet { 35 | tld: None, 36 | display_name: "Default".to_owned(), 37 | use_embedded_node: true, 38 | chain_type: ChainTypes::Mainnet, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use grin_wallet_controller; 4 | 5 | #[derive(thiserror::Error, Debug)] 6 | pub enum GrinWalletInterfaceError { 7 | #[error(transparent)] 8 | WalletController(#[from] grin_wallet_controller::Error), 9 | #[error(transparent)] 10 | WalletLibWallet(#[from] grin_wallet_libwallet::Error), 11 | #[error("Owner API not Instantiated")] 12 | OwnerAPINotInstantiated, 13 | #[error("Foreign API not Instantiated")] 14 | ForeignAPINotInstantiated, 15 | #[error("Invalid Slatepack Address")] 16 | InvalidSlatepackAddress, 17 | #[error("Can't load slatepack file at {file}")] 18 | InvalidSlatepackFile { file: String }, 19 | #[error("Invalid Tx Log State")] 20 | InvalidTxLogState, 21 | #[error("Invalid Invoice Proof")] 22 | InvalidInvoiceProof, 23 | #[error("Invalid Recovery Phrase")] 24 | InvalidRecoveryPhrase, 25 | #[error("Can't read wallet config file at {file}")] 26 | ConfigReadError { file: String }, 27 | } 28 | 29 | #[derive(thiserror::Error, Debug)] 30 | pub enum FilesystemError { 31 | #[error(transparent)] 32 | Io(#[from] std::io::Error), 33 | #[error(transparent)] 34 | SerdeYaml(#[from] serde_yaml::Error), 35 | #[error(transparent)] 36 | Zip(#[from] zip::result::ZipError), 37 | #[error(transparent)] 38 | WalkDir(#[from] walkdir::Error), 39 | #[error("File doesn't exist: {path:?}")] 40 | FileDoesntExist { path: PathBuf }, 41 | #[cfg(target_os = "macos")] 42 | #[error("Could not file bin name {bin_name} in archive")] 43 | BinMissingFromTar { bin_name: String }, 44 | #[error("Failed to normalize path slashes for {path:?}")] 45 | NormalizingPathSlash { path: PathBuf }, 46 | #[error("Could not strip prefix {prefix:?} from {from:?}")] 47 | StripPrefix { prefix: String, from: String }, 48 | } 49 | 50 | #[derive(thiserror::Error, Debug)] 51 | pub enum CacheError { 52 | #[error("No repository information to create cache entry from addon {title}")] 53 | AddonMissingRepo { title: String }, 54 | #[error(transparent)] 55 | Filesystem(#[from] FilesystemError), 56 | } 57 | 58 | #[derive(thiserror::Error, Debug)] 59 | pub enum DownloadError { 60 | #[error("Body len != content len: {body_length} != {content_length}")] 61 | ContentLength { 62 | content_length: u64, 63 | body_length: u64, 64 | }, 65 | #[error("Invalid status code {code} for url {url}")] 66 | InvalidStatusCode { 67 | code: isahc::http::StatusCode, 68 | url: String, 69 | }, 70 | #[error("No new release binary available for {bin_name}")] 71 | MissingSelfUpdateRelease { bin_name: String }, 72 | #[error("Catalog failed to download")] 73 | CatalogFailed, 74 | #[error("Self update for linux only works from AppImage")] 75 | SelfUpdateLinuxNonAppImage, 76 | #[error(transparent)] 77 | Isahc(#[from] isahc::Error), 78 | #[error(transparent)] 79 | Http(#[from] isahc::http::Error), 80 | #[error(transparent)] 81 | Var(#[from] std::env::VarError), 82 | #[error(transparent)] 83 | SerdeJson(#[from] serde_json::Error), 84 | #[error(transparent)] 85 | Filesystem(#[from] FilesystemError), 86 | } 87 | 88 | impl From for DownloadError { 89 | fn from(e: std::io::Error) -> Self { 90 | DownloadError::Filesystem(FilesystemError::Io(e)) 91 | } 92 | } 93 | 94 | #[derive(thiserror::Error, Debug)] 95 | pub enum RepositoryError { 96 | #[error("No repository set for addon")] 97 | AddonNoRepository, 98 | #[error("Failed to parse curse id as u32: {id}")] 99 | CurseIdConversion { id: String }, 100 | #[error("File id must be provided for curse changelog request")] 101 | CurseChangelogFileId, 102 | #[error("No package found for curse id {id}")] 103 | CurseMissingPackage { id: String }, 104 | #[error("No package found for WowI id {id}")] 105 | WowIMissingPackage { id: String }, 106 | #[error("No package found for Hub id {id}")] 107 | HubMissingPackage { id: String }, 108 | #[error("Git repo must be created with `from_source_url`")] 109 | GitWrongConstructor, 110 | #[error("Invalid url {url}")] 111 | GitInvalidUrl { url: String }, 112 | #[error("No valid host in {url}")] 113 | GitMissingHost { url: String }, 114 | #[error("Invalid host {host}, only github.com and gitlab.com are supported")] 115 | GitInvalidHost { host: String }, 116 | #[error("Author not present in {url}")] 117 | GitMissingAuthor { url: String }, 118 | #[error("Repo not present in {url}")] 119 | GitMissingRepo { url: String }, 120 | #[error("No release at {url}")] 121 | GitMissingRelease { url: String }, 122 | #[error("Tag name must be specified for git changelog")] 123 | GitChangelogTagName, 124 | #[error(transparent)] 125 | Download(#[from] DownloadError), 126 | #[error(transparent)] 127 | Filesystem(#[from] FilesystemError), 128 | #[error(transparent)] 129 | Uri(#[from] isahc::http::uri::InvalidUri), 130 | #[error(transparent)] 131 | SerdeJson(#[from] serde_json::Error), 132 | } 133 | 134 | impl From for RepositoryError { 135 | fn from(e: std::io::Error) -> Self { 136 | RepositoryError::Filesystem(FilesystemError::Io(e)) 137 | } 138 | } 139 | 140 | impl From for RepositoryError { 141 | fn from(e: isahc::Error) -> Self { 142 | RepositoryError::Download(DownloadError::Isahc(e)) 143 | } 144 | } 145 | 146 | #[derive(thiserror::Error, Debug)] 147 | pub enum ParseError { 148 | #[error("Addon directory not found: {path:?}")] 149 | MissingAddonDirectory { path: PathBuf }, 150 | #[error("No folders passed to addon")] 151 | BuildAddonEmptyFolders, 152 | #[error("No parent directory for {dir:?}")] 153 | NoParentDirectory { dir: PathBuf }, 154 | #[error("Invalid UTF8 path: {path:?}")] 155 | InvalidUtf8Path { path: PathBuf }, 156 | #[error("Path is not a file or doesn't exist: {path:?}")] 157 | InvalidFile { path: PathBuf }, 158 | #[error("Invalid extension for path: {path:?}")] 159 | InvalidExt { path: PathBuf }, 160 | #[error("Extension not in file parsing regex: {ext}")] 161 | ParsingRegexMissingExt { ext: String }, 162 | #[error("Inclusion regex error for group {group} on pos {pos}, line: {line}")] 163 | InclusionRegexError { 164 | group: usize, 165 | pos: usize, 166 | line: String, 167 | }, 168 | #[error(transparent)] 169 | StripPrefix(#[from] std::path::StripPrefixError), 170 | #[error(transparent)] 171 | GlobPattern(#[from] glob::PatternError), 172 | #[error(transparent)] 173 | Glob(#[from] glob::GlobError), 174 | #[error(transparent)] 175 | FancyRegex(#[from] fancy_regex::Error), 176 | #[error(transparent)] 177 | Download(#[from] DownloadError), 178 | #[error(transparent)] 179 | Filesystem(#[from] FilesystemError), 180 | #[error(transparent)] 181 | Cache(#[from] CacheError), 182 | } 183 | 184 | impl From for ParseError { 185 | fn from(e: std::io::Error) -> Self { 186 | ParseError::Filesystem(FilesystemError::Io(e)) 187 | } 188 | } 189 | 190 | #[derive(thiserror::Error, Debug)] 191 | pub enum ThemeError { 192 | #[error(transparent)] 193 | InvalidUri(#[from] isahc::http::uri::InvalidUri), 194 | #[error(transparent)] 195 | UrlEncoded(#[from] serde_urlencoded::de::Error), 196 | #[error(transparent)] 197 | SerdeYaml(#[from] serde_yaml::Error), 198 | #[error(transparent)] 199 | SerdeJson(#[from] serde_json::Error), 200 | #[error(transparent)] 201 | Io(#[from] std::io::Error), 202 | #[error("Url is missing theme from query")] 203 | MissingQuery, 204 | #[error("Theme already exists with name: {name}")] 205 | NameCollision { name: String }, 206 | } 207 | -------------------------------------------------------------------------------- /crates/core/src/fs/backup.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::backup::BackupFolder; 3 | use crate::error::FilesystemError; 4 | 5 | use path_slash::PathExt; 6 | use std::fs::File; 7 | use std::io::{BufWriter, Read, Write}; 8 | use std::path::{Path, PathBuf}; 9 | use walkdir::WalkDir; 10 | use zip::{write::FileOptions, CompressionMethod, ZipWriter}; 11 | 12 | /// A trait defining a way to back things up to the fs 13 | pub trait Backup { 14 | fn backup(&self) -> Result<()>; 15 | } 16 | 17 | /// Back up folders to a zip archive and save on the fs 18 | pub struct ZipBackup { 19 | src: Vec, 20 | dest: PathBuf, 21 | } 22 | 23 | impl ZipBackup { 24 | pub(crate) fn new(src: Vec, dest: impl AsRef) -> ZipBackup { 25 | ZipBackup { 26 | src, 27 | dest: dest.as_ref().to_owned(), 28 | } 29 | } 30 | } 31 | 32 | impl Backup for ZipBackup { 33 | fn backup(&self) -> Result<()> { 34 | let output = BufWriter::new(File::create(&self.dest)?); 35 | 36 | let mut zip_writer = ZipWriter::new(output); 37 | let options = FileOptions::default() 38 | .compression_method(CompressionMethod::Deflated) 39 | .unix_permissions(0o755); 40 | 41 | let mut buffer = vec![]; 42 | 43 | for folder in &self.src { 44 | let prefix = &folder.prefix; 45 | let path = &folder.path; 46 | 47 | zip_write(path, prefix, &mut buffer, &mut zip_writer, options)?; 48 | 49 | for entry in WalkDir::new(path) 50 | .into_iter() 51 | .filter_map(std::result::Result::ok) 52 | { 53 | let path = entry.path(); 54 | 55 | zip_write(path, prefix, &mut buffer, &mut zip_writer, options)?; 56 | } 57 | } 58 | 59 | zip_writer.finish()?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | /// Write each path to the zip archive 66 | fn zip_write( 67 | path: &Path, 68 | prefix: &Path, 69 | buffer: &mut Vec, 70 | writer: &mut ZipWriter>, 71 | options: FileOptions, 72 | ) -> Result<()> { 73 | if !path.exists() { 74 | return Err(FilesystemError::FileDoesntExist { 75 | path: path.to_owned(), 76 | }); 77 | } 78 | 79 | // On windows, convers `\` to `/` 80 | let normalized_path = path 81 | .to_slash() 82 | .ok_or(FilesystemError::NormalizingPathSlash { 83 | path: path.to_path_buf(), 84 | })?; 85 | let normalized_prefix = prefix 86 | .to_slash() 87 | .ok_or(FilesystemError::NormalizingPathSlash { 88 | path: prefix.to_path_buf(), 89 | })?; 90 | 91 | // Strip prefix from path name and remove leading slash 92 | let name = normalized_path 93 | .strip_prefix(&normalized_prefix) 94 | .ok_or(FilesystemError::StripPrefix { 95 | prefix: normalized_prefix, 96 | from: normalized_path.clone(), 97 | })? 98 | .trim_start_matches('/'); 99 | 100 | if path.is_dir() { 101 | writer.add_directory(name, options)?; 102 | } else { 103 | writer.start_file(name, options)?; 104 | 105 | let mut file = File::open(path)?; 106 | file.read_to_end(buffer)?; 107 | 108 | writer.write_all(buffer)?; 109 | buffer.clear(); 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | pub struct ZstdBackup { 116 | src: Vec, 117 | dest: PathBuf, 118 | level: i32, 119 | } 120 | 121 | impl ZstdBackup { 122 | pub(crate) fn new(src: Vec, dest: impl AsRef, level: i32) -> ZstdBackup { 123 | ZstdBackup { 124 | src, 125 | dest: dest.as_ref().to_owned(), 126 | level, 127 | } 128 | } 129 | } 130 | 131 | impl Backup for ZstdBackup { 132 | fn backup(&self) -> Result<()> { 133 | use zstd::stream::write::Encoder as ZstdEncoder; 134 | 135 | let output = File::create(&self.dest)?; 136 | let mut enc = ZstdEncoder::new(output, self.level)?; 137 | enc.multithread(num_cpus::get() as u32)?; 138 | let mut tar = tar::Builder::new(enc.auto_finish()); 139 | 140 | for folder in &self.src { 141 | let path = folder.path.strip_prefix(&folder.prefix).unwrap(); 142 | let src_path = folder.prefix.join(&folder.path); 143 | tar.append_dir_all(path, src_path)?; 144 | } 145 | tar.finish()?; 146 | 147 | Ok(()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/core/src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::FilesystemError; 2 | 3 | use once_cell::sync::Lazy; 4 | #[cfg(not(windows))] 5 | use std::env; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | use std::sync::Mutex; 9 | 10 | pub mod backup; 11 | mod save; 12 | #[cfg(feature = "default")] 13 | mod theme; 14 | 15 | pub use save::PersistentData; 16 | #[cfg(feature = "default")] 17 | pub use theme::{import_theme, load_user_themes}; 18 | 19 | pub const GRINGUI_CONFIG_DIR: &str = ".grin-gui"; 20 | 21 | pub static CONFIG_DIR: Lazy> = Lazy::new(|| { 22 | // Returns the location of the config directory. Will create if it doesn't 23 | // exist. 24 | // 25 | // $HOME/.config/grin_gui 26 | #[cfg(not(windows))] 27 | { 28 | let home = env::var("HOME").expect("user home directory not found."); 29 | let config_dir = PathBuf::from(&home).join(&format!("{}/gui", GRINGUI_CONFIG_DIR)); 30 | 31 | Mutex::new(config_dir) 32 | } 33 | 34 | // Returns the location of the config directory. Will create if it doesn't 35 | // exist. 36 | // 37 | // %HOME%\grin_gui 38 | #[cfg(windows)] 39 | { 40 | let config_dir = dirs_next::home_dir() 41 | .map(|path| path.join(GRINGUI_CONFIG_DIR)) 42 | .map(|path| path.join("gui")) 43 | .expect("user home directory not found."); 44 | 45 | Mutex::new(config_dir) 46 | } 47 | }); 48 | 49 | pub fn config_dir() -> PathBuf { 50 | let config_dir = CONFIG_DIR.lock().unwrap().clone(); 51 | 52 | if !config_dir.exists() { 53 | let _ = fs::create_dir_all(&config_dir); 54 | } 55 | 56 | config_dir 57 | } 58 | 59 | type Result = std::result::Result; 60 | -------------------------------------------------------------------------------- /crates/core/src/fs/save.rs: -------------------------------------------------------------------------------- 1 | use super::{config_dir, FilesystemError, Result}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | /// Defines a serializable struct that should persist on the filesystem inside the 7 | /// Grin Gui config directory. 8 | pub trait PersistentData: DeserializeOwned + Serialize { 9 | /// Only method required to implement PersistentData on an object. Always relative to 10 | /// the config folder for Grin GUi 11 | fn relative_path() -> PathBuf; 12 | 13 | /// Returns the full file path. Will create any parent directories that don't 14 | /// exist. 15 | fn path() -> Result { 16 | let path = config_dir().join(Self::relative_path()); 17 | 18 | if let Some(dir) = path.parent() { 19 | std::fs::create_dir_all(dir)?; 20 | } 21 | 22 | Ok(path) 23 | } 24 | 25 | /// Load from `PersistentData::path()`. 26 | fn load() -> Result { 27 | let path = Self::path()?; 28 | println!("{:?}", path); 29 | 30 | if path.exists() { 31 | let file = fs::File::open(&path)?; 32 | 33 | Ok(serde_yaml::from_reader(&file)?) 34 | } else { 35 | Err(FilesystemError::FileDoesntExist { path }) 36 | } 37 | } 38 | 39 | /// Load from `PersistentData::path()`. If file doesn't exist, save it to the filesystem as `Default` 40 | /// and return that object. 41 | fn load_or_default() -> Result { 42 | let load_result = ::load(); 43 | match load_result { 44 | Ok(deser) => Ok(deser), 45 | _ => Ok(get_default_and_save()?), 46 | } 47 | } 48 | 49 | /// Save to `PersistentData::path()` 50 | fn save(&self) -> Result<()> { 51 | let contents = serde_yaml::to_string(&self)?; 52 | 53 | fs::write(Self::path()?, contents)?; 54 | 55 | Ok(()) 56 | } 57 | } 58 | 59 | /// Get `Default` and save it. 60 | fn get_default_and_save() -> Result { 61 | let data = Default::default(); 62 | 63 | ::save(&data)?; 64 | 65 | Ok(data) 66 | } 67 | -------------------------------------------------------------------------------- /crates/core/src/fs/theme.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use super::config_dir; 4 | use crate::error::ThemeError; 5 | use crate::theme::Theme; 6 | 7 | use async_std::fs::{self, create_dir_all, read_dir, read_to_string}; 8 | use async_std::stream::StreamExt; 9 | use isahc::http::Uri; 10 | 11 | /// Loads all user defined `.yml` files from the themes 12 | /// folder. Will only return themes that succesfully deserialize 13 | /// from yaml format to `Theme`. 14 | pub async fn load_user_themes() -> Vec { 15 | let mut themes = vec![]; 16 | 17 | let theme_dir = config_dir().join("themes"); 18 | 19 | if !theme_dir.exists() { 20 | let _ = create_dir_all(&theme_dir).await; 21 | } 22 | 23 | if let Ok(mut dir_entries) = read_dir(theme_dir).await { 24 | while let Some(entry) = dir_entries.next().await { 25 | if let Ok(item) = entry { 26 | let path = item.path(); 27 | 28 | let extension = path.extension().unwrap_or_default(); 29 | 30 | if extension == "yaml" || extension == "yml" { 31 | if let Ok(theme_str) = read_to_string(path).await { 32 | if let Ok(theme) = serde_yaml::from_str(&theme_str) { 33 | themes.push(theme); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | log::debug!("loaded {} user themes", themes.len()); 42 | 43 | themes 44 | } 45 | 46 | pub async fn import_theme(url: String) -> Result<(String, Vec), ThemeError> { 47 | let uri = Uri::from_str(&url)?; 48 | 49 | let query = uri.query().ok_or(ThemeError::MissingQuery)?; 50 | 51 | let theme = serde_urlencoded::from_str::>(query)? 52 | .into_iter() 53 | .find(|(name, _)| name == "theme") 54 | .map(|(_, theme_json)| serde_json::from_str::(&theme_json)) 55 | .ok_or(ThemeError::MissingQuery)??; 56 | 57 | let name = &theme.name; 58 | 59 | let theme_dir = config_dir().join("themes"); 60 | 61 | let current_themes = load_user_themes().await; 62 | let shipped_themes = Theme::all(); 63 | 64 | // Check if theme name / filename collision 65 | if current_themes.iter().any(|t| &t.name == name) 66 | || shipped_themes.iter().any(|(t, _)| t == name) 67 | || theme_dir.join(format!("{}.yml", name)).exists() 68 | || theme_dir.join(format!("{}.yaml", name)).exists() 69 | { 70 | return Err(ThemeError::NameCollision { name: name.clone() }); 71 | } 72 | 73 | fs::write( 74 | theme_dir.join(format!("{}.yml", name)), 75 | &serde_yaml::to_vec(&theme)?, 76 | ) 77 | .await?; 78 | 79 | let new_themes = load_user_themes().await; 80 | 81 | Ok((name.clone(), new_themes)) 82 | } 83 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_variables)] 3 | #![allow(unused_must_use)] 4 | 5 | pub mod backup; 6 | pub mod config; 7 | pub mod error; 8 | pub mod fs; 9 | pub mod logger; 10 | pub mod network; 11 | pub mod node; 12 | pub mod theme; 13 | #[cfg(feature = "wgpu")] 14 | pub mod utility; 15 | pub mod wallet; 16 | 17 | #[macro_use] 18 | extern crate lazy_static; 19 | 20 | #[macro_use] 21 | extern crate log; 22 | 23 | // Re-exports 24 | pub use grin_core::consensus::GRIN_BASE; 25 | pub use grin_util::logger::{LogEntry, LoggingConfig}; 26 | -------------------------------------------------------------------------------- /crates/core/src/network.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DownloadError; 2 | use async_std::{fs::File, io::copy}; 3 | use isahc::config::RedirectPolicy; 4 | use isahc::http::header::CONTENT_LENGTH; 5 | use isahc::prelude::*; 6 | use isahc::{HttpClient, Request, Response}; 7 | use once_cell::sync::Lazy; 8 | use serde::Serialize; 9 | use std::path::Path; 10 | 11 | /// Shared `HttpClient`. 12 | static HTTP_CLIENT: Lazy = Lazy::new(|| { 13 | HttpClient::builder() 14 | .redirect_policy(RedirectPolicy::Follow) 15 | .max_connections_per_host(6) 16 | .build() 17 | .unwrap() 18 | }); 19 | 20 | /// Grin Gui user-agent. 21 | fn user_agent() -> String { 22 | format!("grin_gui/{}", env!("CARGO_PKG_VERSION")) 23 | } 24 | 25 | /// Generic request function. 26 | pub async fn request_async( 27 | url: T, 28 | headers: Vec<(&str, &str)>, 29 | timeout: Option, 30 | ) -> Result, DownloadError> { 31 | // Sometimes a download url has a space. 32 | let url = url.to_string().replace(" ", "%20"); 33 | 34 | let mut request = Request::builder().uri(url); 35 | 36 | for (name, value) in headers { 37 | request = request.header(name, value); 38 | } 39 | 40 | request = request.header("user-agent", &user_agent()); 41 | 42 | if let Some(timeout) = timeout { 43 | request = request.timeout(std::time::Duration::from_secs(timeout)); 44 | } 45 | 46 | Ok(HTTP_CLIENT.send_async(request.body(())?).await?) 47 | } 48 | 49 | // Generic function for posting Json data 50 | pub(crate) async fn _post_json_async( 51 | url: T, 52 | data: D, 53 | headers: Vec<(&str, &str)>, 54 | timeout: Option, 55 | ) -> Result, DownloadError> { 56 | let mut request = Request::builder() 57 | .method("POST") 58 | .uri(url.to_string()) 59 | .header("content-type", "application/json"); 60 | 61 | for (name, value) in headers { 62 | request = request.header(name, value); 63 | } 64 | 65 | request = request.header("user-agent", &user_agent()); 66 | 67 | if let Some(timeout) = timeout { 68 | request = request.timeout(std::time::Duration::from_secs(timeout)); 69 | } 70 | 71 | Ok(HTTP_CLIENT 72 | .send_async(request.body(serde_json::to_vec(&data)?)?) 73 | .await?) 74 | } 75 | 76 | /// Download a file from the internet 77 | pub(crate) async fn download_file( 78 | url: T, 79 | dest_file: &Path, 80 | ) -> Result<(), DownloadError> { 81 | let url = url.to_string(); 82 | 83 | log::debug!("downloading file from {}", &url); 84 | 85 | let resp = request_async(&url, vec![("ACCEPT", "application/octet-stream")], None).await?; 86 | let (parts, mut body) = resp.into_parts(); 87 | 88 | // If response length doesn't equal content length, full file wasn't downloaded 89 | // so error out 90 | { 91 | let content_length = parts 92 | .headers 93 | .get(CONTENT_LENGTH) 94 | .map(|v| v.to_str().unwrap_or_default()) 95 | .unwrap_or_default() 96 | .parse::() 97 | .unwrap_or_default(); 98 | 99 | let body_length = body.len().unwrap_or_default(); 100 | 101 | if body_length != content_length { 102 | return Err(DownloadError::ContentLength { 103 | content_length, 104 | body_length, 105 | }); 106 | } 107 | } 108 | 109 | let mut file = File::create(&dest_file).await?; 110 | 111 | copy(&mut body, &mut file).await?; 112 | 113 | log::debug!("file saved as {:?}", &dest_file); 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /crates/core/src/node/subscriber.rs: -------------------------------------------------------------------------------- 1 | use iced_core::Hasher; 2 | use iced_futures::{ 3 | self, 4 | futures::{channel::mpsc, stream::StreamExt}, 5 | subscription, 6 | }; 7 | use std::hash::Hash; 8 | 9 | pub use grin_servers::ServerStats; 10 | 11 | // TODO: Check https://github.com/iced-rs/iced/issues/336 for reference 12 | 13 | #[derive(Clone, Debug)] 14 | pub enum UIMessage { 15 | None, 16 | UpdateStatus(ServerStats), 17 | } 18 | 19 | pub enum State { 20 | Ready, 21 | Listening { receiver: mpsc::Receiver }, 22 | Finished, 23 | } 24 | 25 | pub fn subscriber( 26 | id: I, 27 | ) -> iced::Subscription<(I, UIMessage, Option>)> { 28 | iced::Subscription::from_recipe(NodeSubscriber { id }) 29 | } 30 | 31 | pub struct NodeSubscriber { 32 | id: I, 33 | } 34 | 35 | impl iced_futures::subscription::Recipe for NodeSubscriber 36 | where 37 | T: 'static + Hash + Copy + Send, 38 | { 39 | type Output = (T, UIMessage, Option>); 40 | 41 | fn hash(&self, state: &mut Hasher) { 42 | struct Marker; 43 | std::any::TypeId::of::().hash(state); 44 | self.id.hash(state); 45 | } 46 | 47 | fn stream( 48 | self: Box, 49 | _input: subscription::EventStream, 50 | ) -> futures::stream::BoxStream<'static, Self::Output> { 51 | let id = self.id; 52 | Box::pin(futures::stream::unfold( 53 | State::Ready, 54 | move |state| async move { 55 | match state { 56 | State::Ready => { 57 | let (sender, receiver) = mpsc::channel::(0); 58 | Some(( 59 | (id, UIMessage::None, Some(sender)), 60 | State::Listening { receiver }, 61 | )) 62 | } 63 | State::Listening { mut receiver } => match receiver.next().await { 64 | Some(msg) => Some(((id, msg, None), State::Listening { receiver })), 65 | _ => Some(((id, UIMessage::None, None), State::Listening { receiver })), 66 | }, 67 | State::Finished => { 68 | // Don't let the stream die? 69 | let _: () = iced::futures::future::pending().await; 70 | None 71 | } 72 | } 73 | }, 74 | )) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/core/src/theme/application.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::application; 3 | 4 | impl application::StyleSheet for Theme { 5 | type Style = iced_style::theme::Application; 6 | 7 | fn appearance(&self, style: &Self::Style) -> application::Appearance { 8 | application::Appearance { 9 | background_color: self.palette.base.background, 10 | text_color: self.palette.normal.primary, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/core/src/theme/button.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::button; 2 | use iced::{Background, Color}; 3 | use iced_core::Border; 4 | 5 | use super::Theme; 6 | 7 | #[derive(Debug, Clone, Copy, Default)] 8 | pub enum ButtonStyle { 9 | #[default] 10 | Default, 11 | Bordered, 12 | ColumnHeader, 13 | Primary, 14 | Selected, 15 | SelectedColumn, 16 | NormalText, 17 | } 18 | 19 | impl button::StyleSheet for Theme { 20 | type Style = ButtonStyle; 21 | 22 | fn active(&self, style: &Self::Style) -> button::Appearance { 23 | match style { 24 | ButtonStyle::Default => button::Appearance::default(), 25 | ButtonStyle::Bordered => button::Appearance { 26 | border: Border { 27 | color: self.palette.normal.primary, 28 | width: 1.0, 29 | radius: 2.0.into(), 30 | }, 31 | text_color: self.palette.bright.primary, 32 | ..button::Appearance::default() 33 | }, 34 | ButtonStyle::Primary => button::Appearance { 35 | text_color: self.palette.bright.primary, 36 | border: Border { 37 | color: self.palette.normal.primary, 38 | width: 1.0, 39 | radius: 2.0.into(), 40 | }, 41 | ..Default::default() 42 | }, 43 | ButtonStyle::Selected => button::Appearance { 44 | background: Some(Background::Color(self.palette.normal.primary)), 45 | text_color: self.palette.bright.primary, 46 | border: Border { 47 | color: self.palette.normal.primary, 48 | width: 1.0, 49 | radius: 2.0.into(), 50 | }, 51 | ..button::Appearance::default() 52 | }, 53 | ButtonStyle::NormalText => button::Appearance { 54 | text_color: self.palette.bright.primary, 55 | border: Border { 56 | color: self.palette.normal.primary, 57 | width: 1.0, 58 | radius: 2.0.into(), 59 | }, 60 | ..button::Appearance::default() 61 | }, 62 | ButtonStyle::SelectedColumn => button::Appearance { 63 | background: Some(Background::Color(self.palette.base.background)), 64 | text_color: Color { 65 | ..self.palette.bright.primary 66 | }, 67 | border: Border { 68 | color: self.palette.normal.primary, 69 | width: 1.0, 70 | radius: 2.0.into(), 71 | }, 72 | ..button::Appearance::default() 73 | }, 74 | ButtonStyle::ColumnHeader => button::Appearance { 75 | background: Some(Background::Color(self.palette.base.background)), 76 | border: Border { 77 | color: self.palette.normal.primary, 78 | width: 1.0, 79 | radius: 2.0.into(), 80 | }, 81 | text_color: Color { 82 | ..self.palette.bright.surface 83 | }, 84 | ..button::Appearance::default() 85 | }, 86 | } 87 | } 88 | 89 | fn hovered(&self, style: &Self::Style) -> button::Appearance { 90 | match style { 91 | ButtonStyle::Default => button::Appearance::default(), 92 | ButtonStyle::Bordered => button::Appearance { 93 | background: Some(Background::Color(Color { 94 | a: 0.25, 95 | ..self.palette.normal.primary 96 | })), 97 | text_color: self.palette.bright.primary, 98 | ..self.active(style) 99 | }, 100 | ButtonStyle::Primary => button::Appearance { 101 | background: Some(Background::Color(Color { 102 | a: 0.25, 103 | ..self.palette.normal.primary 104 | })), 105 | text_color: self.palette.bright.primary, 106 | ..self.active(style) 107 | }, 108 | ButtonStyle::Selected => button::Appearance { 109 | background: Some(Background::Color(self.palette.normal.primary)), 110 | text_color: self.palette.bright.primary, 111 | ..self.active(style) 112 | }, 113 | ButtonStyle::NormalText => button::Appearance { 114 | background: Some(Background::Color(Color::TRANSPARENT)), 115 | text_color: self.palette.bright.primary, 116 | ..self.active(style) 117 | }, 118 | ButtonStyle::SelectedColumn => button::Appearance { 119 | background: Some(Background::Color(Color { 120 | a: 0.25, 121 | ..self.palette.normal.primary 122 | })), 123 | text_color: self.palette.bright.primary, 124 | ..self.active(style) 125 | }, 126 | ButtonStyle::ColumnHeader => button::Appearance { 127 | background: Some(Background::Color(Color { 128 | a: 0.15, 129 | ..self.palette.normal.primary 130 | })), 131 | text_color: self.palette.bright.primary, 132 | ..self.active(style) 133 | }, 134 | } 135 | } 136 | 137 | fn disabled(&self, style: &Self::Style) -> button::Appearance { 138 | match style { 139 | ButtonStyle::Default => button::Appearance::default(), 140 | ButtonStyle::Bordered => button::Appearance { 141 | background: Some(Background::Color(Color { 142 | a: 0.05, 143 | ..self.palette.normal.primary 144 | })), 145 | text_color: Color { 146 | a: 0.50, 147 | ..self.palette.bright.primary 148 | }, 149 | ..self.active(style) 150 | }, 151 | ButtonStyle::Primary => button::Appearance { 152 | text_color: Color { 153 | a: 0.25, 154 | ..self.palette.bright.primary 155 | }, 156 | ..self.active(style) 157 | }, 158 | ButtonStyle::Selected => button::Appearance { 159 | ..self.active(style) 160 | }, 161 | _ => self.disabled(style), 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/core/src/theme/card.rs: -------------------------------------------------------------------------------- 1 | use iced::Background; 2 | use iced_aw::card; 3 | use iced_aw::style::card::Appearance; 4 | 5 | use super::Theme; 6 | 7 | #[derive(Debug, Clone, Copy, Default)] 8 | pub enum CardStyle { 9 | #[default] 10 | Default, 11 | Normal, 12 | } 13 | 14 | impl card::StyleSheet for Theme { 15 | type Style = CardStyle; 16 | 17 | fn active(&self, style: &Self::Style) -> Appearance { 18 | match style { 19 | CardStyle::Default => iced_aw::style::card::Appearance::default(), 20 | CardStyle::Normal => Appearance { 21 | background: Background::Color(self.palette.base.background), 22 | head_background: Background::Color(self.palette.normal.primary), 23 | head_text_color: self.palette.bright.surface, 24 | border_color: self.palette.normal.primary, 25 | body_text_color: self.palette.normal.surface, 26 | border_radius: 3.0, 27 | border_width: 0.5, 28 | ..card::Appearance::default() 29 | }, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/core/src/theme/checkbox.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::checkbox; 3 | use iced::Background; 4 | use iced_core::Border; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum CheckboxStyle { 8 | #[default] 9 | Default, 10 | Normal, 11 | } 12 | 13 | impl checkbox::StyleSheet for Theme { 14 | type Style = CheckboxStyle; 15 | 16 | fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { 17 | match style { 18 | CheckboxStyle::Normal => checkbox::Appearance { 19 | background: Background::Color(self.palette.base.background), 20 | icon_color: self.palette.bright.primary, 21 | border: Border { 22 | color: self.palette.normal.primary, 23 | width: 1.0, 24 | radius: 2.0.into(), 25 | }, 26 | text_color: Some(self.palette.normal.surface), 27 | }, 28 | _ => todo!("default"), 29 | } 30 | } 31 | 32 | fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { 33 | match style { 34 | CheckboxStyle::Normal => checkbox::Appearance { 35 | background: Background::Color(self.palette.base.foreground), 36 | icon_color: self.palette.bright.primary, 37 | border: Border { 38 | color: self.palette.normal.primary, 39 | width: 1.0, 40 | radius: 2.0.into(), 41 | }, 42 | text_color: Some(self.palette.normal.surface), 43 | }, 44 | _ => todo!("default"), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/core/src/theme/container.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::container; 3 | use iced::{Background, Color}; 4 | use iced_core::{Border, Shadow, Vector}; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum ContainerStyle { 8 | #[default] 9 | Default, 10 | BrightForeground, 11 | BrightBackground, 12 | ErrorForeground, 13 | NormalBackground, 14 | HoverableForeground, 15 | HoverableBrightForeground, 16 | SuccessBackground, 17 | Segmented, 18 | PanelBordered, 19 | PanelForeground, 20 | } 21 | 22 | impl container::StyleSheet for Theme { 23 | type Style = ContainerStyle; 24 | 25 | fn appearance(&self, style: &Self::Style) -> container::Appearance { 26 | match style { 27 | ContainerStyle::Default => container::Appearance::default(), 28 | ContainerStyle::BrightBackground => container::Appearance { 29 | background: Some(Background::Color(self.palette.base.background)), 30 | text_color: Some(self.palette.bright.surface), 31 | ..container::Appearance::default() 32 | }, 33 | ContainerStyle::BrightForeground => container::Appearance { 34 | background: Some(Background::Color(self.palette.base.foreground)), 35 | text_color: Some(self.palette.bright.surface), 36 | ..container::Appearance::default() 37 | }, 38 | ContainerStyle::ErrorForeground => container::Appearance { 39 | background: Some(Background::Color(self.palette.base.foreground)), 40 | text_color: Some(self.palette.normal.surface), 41 | ..container::Appearance::default() 42 | }, 43 | ContainerStyle::NormalBackground => container::Appearance { 44 | background: Some(Background::Color(self.palette.base.background)), 45 | text_color: Some(self.palette.normal.surface), 46 | ..container::Appearance::default() 47 | }, 48 | ContainerStyle::Segmented => container::Appearance { 49 | text_color: Some(self.palette.bright.primary), 50 | border: Border { 51 | color: self.palette.normal.primary, 52 | width: 1.0, 53 | radius: 2.0.into(), 54 | }, 55 | ..container::Appearance::default() 56 | }, 57 | ContainerStyle::HoverableForeground => container::Appearance { 58 | background: None, 59 | text_color: Some(self.palette.normal.surface), 60 | ..container::Appearance::default() 61 | }, 62 | ContainerStyle::HoverableBrightForeground => container::Appearance { 63 | background: None, 64 | text_color: Some(self.palette.bright.primary), 65 | ..container::Appearance::default() 66 | }, 67 | ContainerStyle::SuccessBackground => container::Appearance { 68 | background: Some(Background::Color(self.palette.base.foreground)), 69 | text_color: Some(self.palette.normal.surface), 70 | ..container::Appearance::default() 71 | }, 72 | ContainerStyle::PanelForeground => container::Appearance { 73 | background: Some(Background::Color(self.palette.base.foreground)), 74 | text_color: Some(self.palette.bright.primary), 75 | border: Border { 76 | color: self.palette.normal.primary, 77 | width: 1.0, 78 | radius: 2.0.into(), 79 | }, 80 | shadow: Shadow { 81 | offset: Vector::new(0.0, 1.0), 82 | blur_radius: 1.0, 83 | color: Color::TRANSPARENT, 84 | }, 85 | }, 86 | ContainerStyle::PanelBordered => container::Appearance { 87 | background: Some(Background::Color(Color::TRANSPARENT)), 88 | text_color: Some(self.palette.bright.primary), 89 | border: Border { 90 | color: self.palette.normal.primary, 91 | width: 1.0, 92 | radius: 2.0.into(), 93 | }, 94 | shadow: Shadow { 95 | offset: Vector::new(0.0, 1.0), 96 | blur_radius: 1.0, 97 | color: Color::TRANSPARENT, 98 | }, 99 | }, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/core/src/theme/modal.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::{Background, Color}; 3 | use iced_aw::style::modal::Appearance; 4 | 5 | #[derive(Clone, Copy, Debug, Default)] 6 | pub enum ModalStyle { 7 | #[default] 8 | Default, 9 | Normal, 10 | } 11 | 12 | impl iced_aw::modal::StyleSheet for Theme { 13 | type Style = ModalStyle; 14 | 15 | fn active(&self, style: &Self::Style) -> Appearance { 16 | match style { 17 | ModalStyle::Normal => Appearance { 18 | background: Background::Color(Color { 19 | a: 0.9, 20 | ..self.palette.base.foreground 21 | }), 22 | }, 23 | _ => Appearance::default(), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/core/src/theme/picklist.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::{Background, Color}; 3 | use iced_core::Border; 4 | use iced_style::{menu, pick_list}; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum PickListStyle { 8 | #[default] 9 | Default, 10 | Primary, 11 | } 12 | 13 | impl pick_list::StyleSheet for Theme { 14 | type Style = PickListStyle; 15 | 16 | fn active(&self, style: &Self::Style) -> pick_list::Appearance { 17 | match style { 18 | PickListStyle::Primary => pick_list::Appearance { 19 | text_color: self.palette.bright.surface, 20 | background: self.palette.base.background.into(), 21 | border: Border { 22 | color: self.palette.normal.primary, 23 | width: 1.0, 24 | radius: 2.0.into(), 25 | }, 26 | handle_color: Color { 27 | a: 0.5, 28 | ..self.palette.normal.primary 29 | }, 30 | placeholder_color: Color { 31 | a: 0.5, 32 | ..self.palette.normal.primary 33 | }, 34 | }, 35 | _ => todo!("default"), 36 | } 37 | } 38 | 39 | fn hovered(&self, style: &Self::Style) -> pick_list::Appearance { 40 | match style { 41 | PickListStyle::Primary => { 42 | let active = self.active(style); 43 | 44 | pick_list::Appearance { 45 | text_color: self.palette.bright.primary, 46 | ..active 47 | } 48 | } 49 | _ => todo!("default"), 50 | } 51 | } 52 | } 53 | 54 | impl menu::StyleSheet for Theme { 55 | type Style = PickListStyle; 56 | 57 | fn appearance(&self, style: &Self::Style) -> menu::Appearance { 58 | match style { 59 | PickListStyle::Primary => menu::Appearance { 60 | text_color: self.palette.bright.surface, 61 | background: Background::Color(self.palette.base.foreground), 62 | border: Border { 63 | color: self.palette.normal.primary, 64 | width: 1.0, 65 | radius: 2.0.into(), 66 | }, 67 | selected_background: Background::Color(Color { 68 | a: 0.15, 69 | ..self.palette.normal.primary 70 | }), 71 | selected_text_color: self.palette.bright.primary, 72 | }, 73 | _ => todo!("default"), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/core/src/theme/radio.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::Color; 3 | use iced_style::radio; 4 | 5 | #[derive(Debug, Clone, Copy, Default)] 6 | pub enum RadioStyle { 7 | #[default] 8 | Default, 9 | Primary, 10 | } 11 | 12 | impl radio::StyleSheet for Theme { 13 | type Style = RadioStyle; 14 | 15 | fn active(&self, style: &Self::Style, is_selected: bool) -> radio::Appearance { 16 | match style { 17 | RadioStyle::Primary => radio::Appearance { 18 | text_color: Some(self.palette.bright.surface), 19 | background: self.palette.base.background.into(), 20 | dot_color: self.palette.bright.surface, 21 | border_width: 1.0, 22 | border_color: Color { 23 | a: 0.5, 24 | ..self.palette.normal.primary 25 | }, 26 | }, 27 | _ => todo!("default"), 28 | } 29 | } 30 | 31 | fn hovered(&self, style: &Self::Style, is_selected: bool) -> radio::Appearance { 32 | match style { 33 | RadioStyle::Primary => { 34 | let active = self.active(style, is_selected); 35 | 36 | radio::Appearance { 37 | text_color: Some(self.palette.bright.primary), 38 | ..active 39 | } 40 | } 41 | _ => todo!("default"), 42 | } 43 | } 44 | } 45 | 46 | /*impl menu::StyleSheet for Theme { 47 | type Style = PickListStyle; 48 | 49 | fn appearance(&self, style: &Self::Style) -> menu::Appearance { 50 | match style { 51 | PickListStyle::Primary => menu::Appearance { 52 | text_color: self.palette.bright.surface, 53 | background: Background::Color(self.palette.base.foreground), 54 | border_width: 1.0, 55 | border_radius: 2.0.into(), 56 | border_color: self.palette.base.background, 57 | selected_background: Background::Color(Color { 58 | a: 0.15, 59 | ..self.palette.normal.primary 60 | }), 61 | selected_text_color: self.palette.bright.primary, 62 | }, 63 | _ => todo!("default") 64 | 65 | } 66 | } 67 | }*/ 68 | -------------------------------------------------------------------------------- /crates/core/src/theme/scrollable.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::scrollable; 3 | use iced::{Background, Color}; 4 | use iced_style::scrollable::{Appearance, Scrollbar, Scroller}; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum ScrollableStyle { 8 | #[default] 9 | Default, 10 | Primary, 11 | } 12 | 13 | impl scrollable::StyleSheet for Theme { 14 | type Style = ScrollableStyle; 15 | 16 | fn active(&self, style: &Self::Style) -> Appearance { 17 | let mut appearance = Appearance { 18 | container: Default::default(), 19 | scrollbar: Scrollbar { 20 | background: None, 21 | border: Default::default(), 22 | scroller: Scroller { 23 | color: self.palette.base.background, 24 | border: Default::default(), 25 | }, 26 | }, 27 | gap: None, 28 | }; 29 | 30 | match style { 31 | ScrollableStyle::Default => { 32 | appearance.scrollbar.background = Some(Background::Color(Color::TRANSPARENT)); 33 | appearance.scrollbar.border.radius = 0.0.into(); 34 | appearance.scrollbar.border.width = 0.0.into(); 35 | appearance.scrollbar.border.color = Color::TRANSPARENT; 36 | } 37 | 38 | ScrollableStyle::Primary => { 39 | appearance.scrollbar.background = 40 | Some(Background::Color(self.palette.base.background)); 41 | appearance.scrollbar.border.radius = 0.0.into(); 42 | appearance.scrollbar.border.width = 0.0.into(); 43 | appearance.scrollbar.border.color = Color::TRANSPARENT; 44 | } 45 | } 46 | 47 | appearance 48 | } 49 | 50 | fn hovered(&self, style: &Self::Style, _is_mouse_over_scrollbar: bool) -> Appearance { 51 | let active = self.active(style); 52 | active 53 | } 54 | 55 | fn dragging(&self, style: &Self::Style) -> Appearance { 56 | let hovered = self.hovered(style, true); 57 | hovered 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/core/src/theme/table_header.rs: -------------------------------------------------------------------------------- 1 | use iced::{Background, Color}; 2 | use iced_aw::style::table_header::Appearance; 3 | use iced_aw::table; 4 | 5 | use super::Theme; 6 | 7 | #[derive(Debug, Clone, Copy, Default)] 8 | pub enum TableHeaderStyle { 9 | #[default] 10 | Default, 11 | } 12 | 13 | impl table::TableHeaderStyleSheet for Theme { 14 | type Style = TableHeaderStyle; 15 | 16 | fn appearance(&self, style: &Self::Style) -> Appearance { 17 | let palette = self.palette; 18 | 19 | match style { 20 | TableHeaderStyle::Default => Appearance { 21 | //text_color: Some(self.palette.bright.surface), 22 | text_color: palette.base.foreground, 23 | background: Some(Background::Color(palette.base.background)), 24 | border_radius: 0.0.into(), 25 | border_width: 0.0, 26 | border_color: Color::TRANSPARENT, 27 | offset_right: 0.0, 28 | offset_left: 0.0, 29 | }, 30 | } 31 | } 32 | 33 | fn hovered(&self, style: &Self::Style) -> Appearance { 34 | let palette = self.palette; 35 | match style { 36 | TableHeaderStyle::Default => Appearance { 37 | background: None, 38 | ..Appearance::default() 39 | }, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/core/src/theme/table_row.rs: -------------------------------------------------------------------------------- 1 | use iced::{Background, Color}; 2 | use iced_aw::style::table_row::{Appearance, RowOrCellAppearance}; 3 | use iced_aw::table; 4 | 5 | use super::Theme; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 8 | pub enum TableRowStyle { 9 | #[default] 10 | Default, 11 | TableRowAlternate, 12 | TableRowHighlight, 13 | TableRowLowlight, 14 | TableRowSelected, 15 | } 16 | 17 | impl table::TableRowStyleSheet for Theme { 18 | type Style = TableRowStyle; 19 | fn appearance(&self, style: &Self::Style, row_id: u16) -> Appearance { 20 | let palette = self.palette; 21 | 22 | match style { 23 | TableRowStyle::Default => Appearance { 24 | row: RowOrCellAppearance { 25 | text_color: palette.normal.primary, 26 | background: Some(Background::Color(palette.base.foreground)), 27 | border_radius: 0.0.into(), 28 | border_width: 0.0, 29 | border_color: Color::TRANSPARENT, 30 | offset_left: 0.0, 31 | offset_right: 0.0, 32 | }, 33 | cell: RowOrCellAppearance { 34 | text_color: palette.normal.primary, 35 | background: None, 36 | border_radius: 0.0.into(), 37 | border_width: 0.0, 38 | border_color: Color::TRANSPARENT, 39 | offset_left: 0.0, 40 | offset_right: 0.0, 41 | }, 42 | }, 43 | TableRowStyle::TableRowAlternate => Appearance { 44 | row: RowOrCellAppearance { 45 | text_color: palette.normal.primary, 46 | background: Some(Background::Color(Color { 47 | a: 0.50, 48 | ..palette.base.foreground 49 | })), 50 | ..RowOrCellAppearance::default() 51 | }, 52 | cell: RowOrCellAppearance { 53 | text_color: palette.normal.primary, 54 | background: None, 55 | ..RowOrCellAppearance::default() 56 | }, 57 | }, 58 | TableRowStyle::TableRowHighlight => Appearance { 59 | row: RowOrCellAppearance { 60 | text_color: palette.normal.primary, 61 | background: Some(Background::Color(Color { 62 | a: 0.30, 63 | ..palette.base.foreground 64 | })), 65 | border_radius: 0.0.into(), 66 | border_width: 0.0, 67 | border_color: Color::TRANSPARENT, 68 | offset_left: 0.0, 69 | offset_right: 0.0, 70 | }, 71 | cell: RowOrCellAppearance { 72 | text_color: palette.normal.primary, 73 | background: None, 74 | border_radius: 0.0.into(), 75 | border_width: 0.0, 76 | border_color: Color::TRANSPARENT, 77 | offset_left: 0.0, 78 | offset_right: 0.0, 79 | }, 80 | }, 81 | TableRowStyle::TableRowLowlight => Appearance { 82 | row: RowOrCellAppearance { 83 | text_color: palette.normal.primary, 84 | background: Some(Background::Color(Color::TRANSPARENT)), 85 | border_radius: 0.0.into(), 86 | border_width: 0.0, 87 | border_color: Color::TRANSPARENT, 88 | offset_left: 0.0, 89 | offset_right: 0.0, 90 | }, 91 | cell: RowOrCellAppearance { 92 | text_color: palette.normal.primary, 93 | background: None, 94 | border_radius: 0.0.into(), 95 | border_width: 0.0, 96 | border_color: Color::TRANSPARENT, 97 | offset_left: 0.0, 98 | offset_right: 0.0, 99 | }, 100 | }, 101 | TableRowStyle::TableRowSelected => Appearance { 102 | row: RowOrCellAppearance { 103 | text_color: palette.normal.primary, 104 | background: Some(Background::Color(palette.normal.primary)), 105 | border_radius: 0.0.into(), 106 | border_width: 0.0, 107 | border_color: Color::TRANSPARENT, 108 | offset_left: 0.0, 109 | offset_right: 0.0, 110 | }, 111 | cell: RowOrCellAppearance { 112 | text_color: palette.normal.primary, 113 | background: None, 114 | border_radius: 0.0.into(), 115 | border_width: 0.0, 116 | border_color: Color::TRANSPARENT, 117 | offset_left: 0.0, 118 | offset_right: 0.0, 119 | }, 120 | }, 121 | } 122 | } 123 | 124 | fn hovered(&self, style: &Self::Style, row_id: u16) -> Appearance { 125 | let palette = self.palette; 126 | match style { 127 | TableRowStyle::Default => Appearance { 128 | row: RowOrCellAppearance { 129 | background: Some(Background::Color(Color { 130 | a: 0.60, 131 | ..palette.normal.primary 132 | })), 133 | ..self.appearance(style, row_id).row 134 | }, 135 | cell: RowOrCellAppearance { 136 | background: None, 137 | ..self.appearance(style, row_id).cell 138 | }, 139 | }, 140 | TableRowStyle::TableRowAlternate => Appearance { 141 | row: RowOrCellAppearance { 142 | background: Some(Background::Color(Color { 143 | a: 0.25, 144 | ..palette.normal.primary 145 | })), 146 | ..self.appearance(style, row_id).row 147 | }, 148 | cell: RowOrCellAppearance { 149 | background: None, 150 | ..self.appearance(style, row_id).cell 151 | }, 152 | }, 153 | TableRowStyle::TableRowHighlight => Appearance { 154 | row: RowOrCellAppearance { 155 | background: Some(Background::Color(Color { 156 | a: 0.60, 157 | ..palette.normal.primary 158 | })), 159 | ..self.appearance(style, row_id).row 160 | }, 161 | cell: RowOrCellAppearance { 162 | background: None, 163 | ..self.appearance(style, row_id).cell 164 | }, 165 | }, 166 | TableRowStyle::TableRowLowlight => Appearance { 167 | row: RowOrCellAppearance { 168 | background: Some(Background::Color(Color { 169 | a: 0.60, 170 | ..palette.normal.primary 171 | })), 172 | ..self.appearance(style, row_id).row 173 | }, 174 | cell: RowOrCellAppearance { 175 | background: None, 176 | ..self.appearance(style, row_id).cell 177 | }, 178 | }, 179 | TableRowStyle::TableRowSelected => Appearance { 180 | row: RowOrCellAppearance { 181 | background: Some(Background::Color(Color { 182 | a: 0.60, 183 | ..palette.normal.primary 184 | })), 185 | ..self.appearance(style, row_id).row 186 | }, 187 | cell: RowOrCellAppearance { 188 | background: None, 189 | ..self.appearance(style, row_id).cell 190 | }, 191 | }, 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /crates/core/src/theme/text.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::text; 3 | 4 | #[derive(Debug, Clone, Copy, Default)] 5 | pub enum TextStyle { 6 | #[default] 7 | Default, 8 | Warning, 9 | } 10 | 11 | impl text::StyleSheet for Theme { 12 | type Style = TextStyle; 13 | 14 | fn appearance(&self, style: Self::Style) -> text::Appearance { 15 | match style { 16 | TextStyle::Warning => text::Appearance { 17 | color: Some(self.palette.bright.error), 18 | }, 19 | TextStyle::Default => Default::default(), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/core/src/theme/text_editor.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::text_editor; 3 | use iced::{Background, Color}; 4 | use iced_core::Border; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum TextEditorStyle { 8 | #[default] 9 | Default, 10 | } 11 | 12 | impl text_editor::StyleSheet for Theme { 13 | type Style = TextEditorStyle; 14 | 15 | /// Produces the style of an active text input. 16 | fn active(&self, style: &Self::Style) -> text_editor::Appearance { 17 | match style { 18 | TextEditorStyle::Default => text_editor::Appearance { 19 | background: Background::Color(self.palette.base.foreground), 20 | border: Border { 21 | color: self.palette.normal.primary, 22 | width: 1.0, 23 | radius: 2.0.into(), 24 | }, 25 | }, 26 | } 27 | } 28 | 29 | /// Produces the style of a focused text input. 30 | fn focused(&self, style: &Self::Style) -> text_editor::Appearance { 31 | match style { 32 | TextEditorStyle::Default => text_editor::Appearance { 33 | background: Background::Color(self.palette.base.foreground), 34 | border: Border { 35 | color: self.palette.bright.primary, 36 | width: 1.0, 37 | radius: 2.0.into(), 38 | }, 39 | }, 40 | } 41 | } 42 | 43 | fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { 44 | match style { 45 | TextEditorStyle::Default => text_editor::Appearance { 46 | background: Background::Color(self.palette.base.foreground), 47 | border: Border { 48 | color: self.palette.normal.primary, 49 | width: 1.0, 50 | radius: 2.0.into(), 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | fn placeholder_color(&self, style: &Self::Style) -> Color { 57 | match style { 58 | TextEditorStyle::Default => self.palette.normal.surface, 59 | _ => todo!("default"), 60 | } 61 | } 62 | 63 | fn value_color(&self, style: &Self::Style) -> Color { 64 | match style { 65 | TextEditorStyle::Default => self.palette.bright.primary, 66 | _ => todo!("default"), 67 | } 68 | } 69 | 70 | fn selection_color(&self, style: &Self::Style) -> Color { 71 | match style { 72 | TextEditorStyle::Default => self.palette.bright.secondary, 73 | _ => todo!("default"), 74 | } 75 | } 76 | 77 | fn disabled_color(&self, style: &Self::Style) -> Color { 78 | match style { 79 | TextEditorStyle::Default => self.palette.normal.secondary, 80 | _ => todo!("default"), 81 | } 82 | } 83 | 84 | /// Produces the style of an hovered text editor. 85 | fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { 86 | self.focused(style) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/core/src/theme/text_input.rs: -------------------------------------------------------------------------------- 1 | use super::Theme; 2 | use iced::widget::text_input; 3 | use iced::{Background, Color}; 4 | use iced_core::Border; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum TextInputStyle { 8 | #[default] 9 | Default, 10 | AddonsQuery, 11 | } 12 | 13 | impl text_input::StyleSheet for Theme { 14 | type Style = TextInputStyle; 15 | 16 | /// Produces the style of an active text input. 17 | fn active(&self, style: &Self::Style) -> text_input::Appearance { 18 | match style { 19 | TextInputStyle::AddonsQuery => text_input::Appearance { 20 | background: Background::Color(self.palette.base.foreground), 21 | border: Border { 22 | color: self.palette.normal.primary, 23 | width: 1.0, 24 | radius: 2.0.into(), 25 | }, 26 | icon_color: self.palette.base.foreground, 27 | }, 28 | _ => todo!("default"), 29 | } 30 | } 31 | 32 | /// Produces the style of a focused text input. 33 | fn focused(&self, style: &Self::Style) -> text_input::Appearance { 34 | match style { 35 | TextInputStyle::AddonsQuery => text_input::Appearance { 36 | background: Background::Color(self.palette.base.foreground), 37 | border: Border { 38 | color: self.palette.bright.primary, 39 | width: 1.0, 40 | radius: 2.0.into(), 41 | }, 42 | icon_color: Color { 43 | a: 1.0, 44 | ..self.palette.normal.primary 45 | }, 46 | }, 47 | _ => todo!("default"), 48 | } 49 | } 50 | 51 | fn disabled(&self, style: &Self::Style) -> text_input::Appearance { 52 | match style { 53 | TextInputStyle::AddonsQuery => text_input::Appearance { 54 | background: Background::Color(self.palette.base.foreground), 55 | border: Border { 56 | color: self.palette.normal.primary, 57 | width: 1.0, 58 | radius: 2.0.into(), 59 | }, 60 | icon_color: Color { 61 | a: 1.0, 62 | ..self.palette.normal.primary 63 | }, 64 | }, 65 | _ => todo!("default"), 66 | } 67 | } 68 | 69 | fn placeholder_color(&self, style: &Self::Style) -> Color { 70 | match style { 71 | TextInputStyle::AddonsQuery => self.palette.normal.surface, 72 | _ => todo!("default"), 73 | } 74 | } 75 | 76 | fn value_color(&self, style: &Self::Style) -> Color { 77 | match style { 78 | TextInputStyle::AddonsQuery => self.palette.bright.primary, 79 | _ => todo!("default"), 80 | } 81 | } 82 | 83 | fn selection_color(&self, style: &Self::Style) -> Color { 84 | match style { 85 | TextInputStyle::AddonsQuery => self.palette.bright.secondary, 86 | _ => todo!("default"), 87 | } 88 | } 89 | 90 | fn disabled_color(&self, style: &Self::Style) -> Color { 91 | match style { 92 | TextInputStyle::AddonsQuery => self.palette.normal.secondary, 93 | _ => todo!("default"), 94 | } 95 | } 96 | 97 | /// Produces the style of an hovered text input. 98 | fn hovered(&self, style: &Self::Style) -> text_input::Appearance { 99 | self.focused(style) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/core/src/utility.rs: -------------------------------------------------------------------------------- 1 | use crate::config::SelfUpdateChannel; 2 | use crate::error::DownloadError; 3 | #[cfg(target_os = "macos")] 4 | use crate::error::FilesystemError; 5 | use crate::network::download_file; 6 | 7 | use regex::Regex; 8 | use retry::delay::Fibonacci; 9 | use retry::{retry, Error as RetryError, OperationResult}; 10 | use serde::Deserialize; 11 | 12 | use std::fs; 13 | use std::io; 14 | use std::path::{Path, PathBuf}; 15 | 16 | /// Takes a `&str` and formats it into a proper 17 | /// World of Warcraft release version. 18 | /// 19 | /// Eg. 90001 would be 9.0.1. 20 | pub fn format_interface_into_game_version(interface: &str) -> String { 21 | if interface.len() == 5 { 22 | let major = interface[..1].parse::(); 23 | let minor = interface[1..3].parse::(); 24 | let patch = interface[3..5].parse::(); 25 | if let (Ok(major), Ok(minor), Ok(patch)) = (major, minor, patch) { 26 | return format!("{}.{}.{}", major, minor, patch); 27 | } 28 | } 29 | 30 | interface.to_owned() 31 | } 32 | 33 | /// Takes a `&str` and strips any non-digit. 34 | /// This is used to unify and compare addon versions: 35 | /// 36 | /// A string looking like 213r323 would return 213323. 37 | /// A string looking like Rematch_4_10_15.zip would return 41015. 38 | pub(crate) fn _strip_non_digits(string: &str) -> String { 39 | let re = Regex::new(r"[\D]").unwrap(); 40 | let stripped = re.replace_all(string, "").to_string(); 41 | stripped 42 | } 43 | 44 | #[derive(Debug, Deserialize, Clone)] 45 | pub struct Release { 46 | pub tag_name: String, 47 | pub prerelease: bool, 48 | pub assets: Vec, 49 | pub body: String, 50 | } 51 | 52 | #[derive(Debug, Deserialize, Clone)] 53 | pub struct ReleaseAsset { 54 | pub name: String, 55 | #[serde(rename = "browser_download_url")] 56 | pub download_url: String, 57 | } 58 | 59 | #[cfg(feature = "no-self-update")] 60 | pub async fn get_latest_release(_channel: SelfUpdateChannel) -> Option { 61 | None 62 | } 63 | 64 | #[cfg(not(feature = "no-self-update"))] 65 | pub async fn get_latest_release(channel: SelfUpdateChannel) -> Option { 66 | use crate::network::request_async; 67 | use isahc::AsyncReadResponseExt; 68 | 69 | log::debug!("checking for application update"); 70 | 71 | let mut resp = request_async( 72 | "https://api.github.com/repos/grin-gui/grin-gui/releases", 73 | vec![], 74 | None, 75 | ) 76 | .await 77 | .ok()?; 78 | 79 | let releases: Vec = resp.json().await.ok()?; 80 | 81 | releases.into_iter().find(|r| { 82 | if channel == SelfUpdateChannel::Beta { 83 | // If beta, always want latest release 84 | true 85 | } else { 86 | // Otherwise ONLY non-prereleases 87 | !r.prerelease 88 | } 89 | }) 90 | } 91 | 92 | /// Downloads the latest release file that matches `bin_name`, renames the current 93 | /// executable to a temp path, renames the new version as the original file name, 94 | /// then returns both the original file name (new version) and temp path (old version) 95 | pub async fn download_update_to_temp_file( 96 | bin_name: String, 97 | release: Release, 98 | ) -> Result<(PathBuf, PathBuf), DownloadError> { 99 | #[cfg(not(target_os = "linux"))] 100 | let current_bin_path = std::env::current_exe()?; 101 | 102 | #[cfg(target_os = "linux")] 103 | let current_bin_path = PathBuf::from( 104 | std::env::var("APPIMAGE").map_err(|_| DownloadError::SelfUpdateLinuxNonAppImage)?, 105 | ); 106 | 107 | // Path to download the new version to 108 | let download_path = current_bin_path 109 | .parent() 110 | .unwrap() 111 | .join(&format!("tmp_{}", bin_name)); 112 | 113 | // Path to temporarily force rename current process to, se we can then 114 | // rename `download_path` to `current_bin_path` and then launch new version 115 | // cleanly as `current_bin_path` 116 | let tmp_path = current_bin_path 117 | .parent() 118 | .unwrap() 119 | .join(&format!("tmp2_{}", bin_name)); 120 | 121 | // On macos, we actually download an archive with the new binary inside. Let's extract 122 | // that file and remove the archive. 123 | #[cfg(target_os = "macos")] 124 | { 125 | let asset_name = format!("{}-macos.tar.gz", bin_name); 126 | 127 | let asset = release 128 | .assets 129 | .iter() 130 | .find(|a| a.name == asset_name) 131 | .cloned() 132 | .ok_or(DownloadError::MissingSelfUpdateRelease { bin_name })?; 133 | 134 | let archive_path = current_bin_path.parent().unwrap().join(&asset_name); 135 | 136 | download_file(&asset.download_url, &archive_path).await?; 137 | 138 | extract_binary_from_tar(&archive_path, &download_path, "grin-gui")?; 139 | 140 | std::fs::remove_file(&archive_path)?; 141 | } 142 | 143 | // For windows & linux, we download the new binary directly 144 | #[cfg(not(target_os = "macos"))] 145 | { 146 | let asset = release 147 | .assets 148 | .iter() 149 | .find(|a| a.name == bin_name) 150 | .cloned() 151 | .ok_or(DownloadError::MissingSelfUpdateRelease { bin_name })?; 152 | 153 | download_file(&asset.download_url, &download_path).await?; 154 | } 155 | 156 | // Make executable 157 | #[cfg(not(target_os = "windows"))] 158 | { 159 | use async_std::fs; 160 | use std::os::unix::fs::PermissionsExt; 161 | 162 | let mut permissions = fs::metadata(&download_path).await?.permissions(); 163 | permissions.set_mode(0o755); 164 | fs::set_permissions(&download_path, permissions).await?; 165 | } 166 | 167 | rename(¤t_bin_path, &tmp_path)?; 168 | 169 | rename(&download_path, ¤t_bin_path)?; 170 | 171 | Ok((current_bin_path, tmp_path)) 172 | } 173 | 174 | /// Extracts the Grin Gui binary from a `tar.gz` archive to temp_file path 175 | #[cfg(target_os = "macos")] 176 | fn extract_binary_from_tar( 177 | archive_path: &Path, 178 | temp_file: &Path, 179 | bin_name: &str, 180 | ) -> Result<(), FilesystemError> { 181 | use flate2::read::GzDecoder; 182 | use std::fs::File; 183 | use std::io::copy; 184 | use tar::Archive; 185 | 186 | let mut archive = Archive::new(GzDecoder::new(File::open(&archive_path)?)); 187 | 188 | let mut temp_file = File::create(temp_file)?; 189 | 190 | for file in archive.entries()? { 191 | let mut file = file?; 192 | 193 | let path = file.path()?; 194 | 195 | if let Some(name) = path.to_str() { 196 | if name == bin_name { 197 | copy(&mut file, &mut temp_file)?; 198 | 199 | return Ok(()); 200 | } 201 | } 202 | } 203 | 204 | Err(FilesystemError::BinMissingFromTar { 205 | bin_name: bin_name.to_owned(), 206 | }) 207 | } 208 | 209 | /// Rename a file or directory to a new name, retrying if the operation fails because of permissions 210 | /// 211 | /// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan 212 | /// and other automated operations to complete. 213 | pub fn rename(from: F, to: T) -> io::Result<()> 214 | where 215 | F: AsRef, 216 | T: AsRef, 217 | { 218 | // 21 Fibonacci steps starting at 1 ms is ~28 seconds total 219 | // See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around 220 | // virus scanning file locks 221 | let from = from.as_ref(); 222 | let to = to.as_ref(); 223 | 224 | retry(Fibonacci::from_millis(1).take(21), || { 225 | match fs::rename(from, to) { 226 | Ok(_) => OperationResult::Ok(()), 227 | Err(e) => match e.kind() { 228 | io::ErrorKind::PermissionDenied => OperationResult::Retry(e), 229 | _ => OperationResult::Err(e), 230 | }, 231 | } 232 | }) 233 | .map_err(|e| match e { 234 | RetryError::Operation { error, .. } => error, 235 | RetryError::Internal(message) => io::Error::new(io::ErrorKind::Other, message), 236 | }) 237 | } 238 | 239 | /// Remove a file, retrying if the operation fails because of permissions 240 | /// 241 | /// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan 242 | /// and other automated operations to complete. 243 | pub fn remove_file

(path: P) -> io::Result<()> 244 | where 245 | P: AsRef, 246 | { 247 | // 21 Fibonacci steps starting at 1 ms is ~28 seconds total 248 | // See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around 249 | // virus scanning file locks 250 | let path = path.as_ref(); 251 | 252 | retry( 253 | Fibonacci::from_millis(1).take(21), 254 | || match fs::remove_file(path) { 255 | Ok(_) => OperationResult::Ok(()), 256 | Err(e) => match e.kind() { 257 | io::ErrorKind::PermissionDenied => OperationResult::Retry(e), 258 | _ => OperationResult::Err(e), 259 | }, 260 | }, 261 | ) 262 | .map_err(|e| match e { 263 | RetryError::Operation { error, .. } => error, 264 | RetryError::Internal(message) => io::Error::new(io::ErrorKind::Other, message), 265 | }) 266 | } 267 | 268 | pub(crate) fn _truncate(s: &str, max_chars: usize) -> &str { 269 | match s.char_indices().nth(max_chars) { 270 | None => s, 271 | Some((idx, _)) => &s[..idx], 272 | } 273 | } 274 | 275 | pub(crate) fn _regex_html_tags_to_newline() -> Regex { 276 | regex::Regex::new(r"
|#.\s").unwrap() 277 | } 278 | 279 | pub(crate) fn _regex_html_tags_to_space() -> Regex { 280 | regex::Regex::new(r"<[^>]*>|&#?\w+;|[gl]t;").unwrap() 281 | } 282 | 283 | #[cfg(test)] 284 | mod tests { 285 | use super::*; 286 | 287 | #[test] 288 | fn test_interface() { 289 | let interface = "90001"; 290 | assert_eq!("9.0.1", format_interface_into_game_version(interface)); 291 | 292 | let interface = "11305"; 293 | assert_eq!("1.13.5", format_interface_into_game_version(interface)); 294 | 295 | let interface = "100000"; 296 | assert_eq!("100000", format_interface_into_game_version(interface)); 297 | 298 | let interface = "9.0.1"; 299 | assert_eq!("9.0.1", format_interface_into_game_version(interface)); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /fonts/notosans-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/fonts/notosans-bold.ttf -------------------------------------------------------------------------------- /fonts/notosans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/fonts/notosans-regular.ttf -------------------------------------------------------------------------------- /resources/linux/grin-gui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Terminal=false 4 | Exec=grin-gui 5 | Name=grin-gui 6 | Icon=grin 7 | Categories=Game; 8 | -------------------------------------------------------------------------------- /resources/logo/1024x1024/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/1024x1024/grin.png -------------------------------------------------------------------------------- /resources/logo/1024x1024/grin_macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/1024x1024/grin_macos.png -------------------------------------------------------------------------------- /resources/logo/128x128/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/128x128/grin.png -------------------------------------------------------------------------------- /resources/logo/16x16/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/16x16/grin.png -------------------------------------------------------------------------------- /resources/logo/24x24/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/24x24/grin.png -------------------------------------------------------------------------------- /resources/logo/256x256/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/256x256/grin.png -------------------------------------------------------------------------------- /resources/logo/32x32/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/32x32/grin.png -------------------------------------------------------------------------------- /resources/logo/48x48/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/48x48/grin.png -------------------------------------------------------------------------------- /resources/logo/512x512/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/512x512/grin.png -------------------------------------------------------------------------------- /resources/logo/64x64/grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/logo/64x64/grin.png -------------------------------------------------------------------------------- /resources/osx/GrinGUI.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | grin-gui 9 | CFBundleIdentifier 10 | org.mimblewimble.grin.grin-gui 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | GrinGUI 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSupportedPlatforms 20 | 21 | MacOSX 22 | 23 | CFBundleVersion 24 | 1 25 | CFBundleIconFile 26 | grin-gui.icns 27 | NSHighResolutionCapable 28 | 29 | NSMainNibFile 30 | 31 | NSSupportsAutomaticGraphicsSwitching 32 | 33 | CFBundleDisplayName 34 | GrinGUI 35 | NSRequiresAquaSystemAppearance 36 | NO 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/osx/GrinGUI.app/Contents/Resources/grin-gui.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/osx/GrinGUI.app/Contents/Resources/grin-gui.icns -------------------------------------------------------------------------------- /resources/windows/grin.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-gui/a6ccef989a6993f77556f19cf106b6d8fbeea0d8/resources/windows/grin.ico -------------------------------------------------------------------------------- /resources/windows/res.rc: -------------------------------------------------------------------------------- 1 | #define IDI_ICON 0x101 2 | 3 | IDI_ICON ICON "grin.ico" -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | edition = "2021" -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::VERSION; 2 | 3 | use isahc::http::Uri; 4 | use structopt::{ 5 | clap::{self, AppSettings}, 6 | StructOpt, 7 | }; 8 | 9 | use std::env; 10 | use std::path::PathBuf; 11 | 12 | pub fn get_opts() -> Result { 13 | let args = env::args_os(); 14 | 15 | Opts::from_iter_safe(args) 16 | } 17 | 18 | #[allow(unused_variables)] 19 | pub fn validate_opts_or_exit( 20 | opts_result: Result, 21 | is_cli: bool, 22 | is_debug: bool, 23 | ) -> Opts { 24 | // If an error, we need to setup the AttachConsole fix for Windows release 25 | // so we can exit and display the error message to the user. 26 | let is_opts_error = opts_result.is_err(); 27 | 28 | // Workaround to output to console even though we compile with windows_subsystem = "windows" 29 | // in release mode 30 | #[cfg(target_os = "windows")] 31 | { 32 | if (is_cli || is_opts_error) && !is_debug { 33 | use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS}; 34 | 35 | unsafe { 36 | AttachConsole(ATTACH_PARENT_PROCESS); 37 | } 38 | } 39 | } 40 | 41 | // Now that the windows fix is successfully setup, we can safely exit on the error 42 | // and it will display properly to the user, or carry forward with the program 43 | // and properly display our logging to the user. Since `e.exit()` returns a `!`, 44 | // we can return the Ok(Opts) and carry forward with our program. 45 | match opts_result { 46 | Ok(opts) => opts, 47 | Err(e) => { 48 | // Apparently on `--version`, there is no "error message" that gets displayed 49 | // like with `--help` and actual clap errors. It gets printed before we 50 | // ever hit our console fix for windows, so let's manually print it 51 | // before exiting 52 | #[cfg(target_os = "windows")] 53 | { 54 | if !is_debug && e.kind == clap::ErrorKind::VersionDisplayed { 55 | print!("Grin GUI {}", VERSION); 56 | } 57 | } 58 | 59 | e.exit(); 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, StructOpt)] 65 | #[structopt(name = "GrinGUI", 66 | about = env!("CARGO_PKG_DESCRIPTION"), 67 | version = VERSION, 68 | author = env!("CARGO_PKG_AUTHORS"), 69 | setting = AppSettings::DisableHelpSubcommand)] 70 | pub struct Opts { 71 | #[structopt(long = "data", help = "Path to a custom data directory for the app")] 72 | pub data_directory: Option, 73 | #[structopt(long = "aa", help = "Enable / Disable Anti-aliasing (true / false)")] 74 | pub antialiasing: Option, 75 | #[structopt(subcommand)] 76 | pub command: Option, 77 | #[structopt(long, hidden = true)] 78 | pub self_update_temp: Option, 79 | } 80 | 81 | #[derive(Debug, StructOpt)] 82 | pub enum Command {} 83 | -------------------------------------------------------------------------------- /src/gui/element/about.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING}, 3 | crate::gui::{Interaction, Message}, 4 | crate::localization::localized_string, 5 | grin_gui_core::theme::{Button, Column, Container, Element, PickList, Row, Scrollable, Text}, 6 | grin_gui_core::{theme::ColorPalette, utility::Release}, 7 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space, TextInput}, 8 | iced::{alignment, Alignment, Command, Length}, 9 | std::collections::HashMap, 10 | strfmt::strfmt, 11 | }; 12 | 13 | pub struct StateContainer {} 14 | 15 | impl Default for StateContainer { 16 | fn default() -> Self { 17 | Self {} 18 | } 19 | } 20 | 21 | pub fn data_container<'a>( 22 | release: &Option, 23 | state: &'a StateContainer, 24 | ) -> Container<'a, Message> { 25 | let grin_gui_title = Text::new(localized_string("grin")).size(DEFAULT_HEADER_FONT_SIZE); 26 | let grin_gui_title_container = Container::new(grin_gui_title) 27 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 28 | 29 | let changelog_title_text = Text::new(if let Some(release) = release { 30 | let mut vars = HashMap::new(); 31 | // TODO (casperstorm): change "addon" to "tag" or "version". 32 | vars.insert("addon".to_string(), &release.tag_name); 33 | let fmt = localized_string("changelog-for"); 34 | strfmt(&fmt, &vars).unwrap() 35 | } else { 36 | localized_string("changelog") 37 | }) 38 | .size(DEFAULT_FONT_SIZE); 39 | 40 | let changelog_text = Text::new(if let Some(release) = release { 41 | release.body.clone() 42 | } else { 43 | localized_string("no-changelog") 44 | }) 45 | .size(DEFAULT_FONT_SIZE); 46 | 47 | let website_button: Element = 48 | Button::new(Text::new(localized_string("website")).size(DEFAULT_FONT_SIZE)) 49 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 50 | .on_press(Interaction::OpenLink(localized_string("website-http"))) 51 | .into(); 52 | 53 | let donation_button: Element = 54 | Button::new(Text::new(localized_string("donate")).size(DEFAULT_FONT_SIZE)) 55 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 56 | .on_press(Interaction::OpenLink(localized_string("donate-http"))) 57 | .into(); 58 | 59 | let button_row = Row::new() 60 | .spacing(DEFAULT_PADDING) 61 | .push(website_button.map(Message::Interaction)) 62 | .push(donation_button.map(Message::Interaction)); 63 | 64 | let changelog_text_container = Container::new(changelog_text) 65 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 66 | let changelog_title_container = Container::new(changelog_title_text) 67 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 68 | 69 | let column = Column::new() 70 | .spacing(1) 71 | .push(grin_gui_title_container) 72 | .push(Space::new( 73 | Length::Fixed(0.0), 74 | Length::Fixed(DEFAULT_PADDING), 75 | )) 76 | .push(button_row) 77 | .push(Space::new( 78 | Length::Fixed(0.0), 79 | Length::Fixed(DEFAULT_PADDING), 80 | )) 81 | .push(changelog_title_container) 82 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 83 | .push(changelog_text_container); 84 | 85 | let mut scrollable = Scrollable::new(column) 86 | .height(Length::FillPortion(1)) 87 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 88 | 89 | let col = Column::new().push(scrollable); 90 | let row = Row::new() 91 | .push(Space::new( 92 | Length::Fixed(DEFAULT_PADDING), 93 | Length::Fixed(0.0), 94 | )) 95 | .push(col); 96 | 97 | // Returns the final container. 98 | Container::new(row) 99 | .center_x() 100 | .width(Length::Fill) 101 | .height(Length::Shrink) 102 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 103 | .padding(20) 104 | } 105 | -------------------------------------------------------------------------------- /src/gui/element/menu.rs: -------------------------------------------------------------------------------- 1 | use iced_style::container::StyleSheet; 2 | use { 3 | super::{DEFAULT_FONT_SIZE, DEFAULT_PADDING, SMALLER_FONT_SIZE}, 4 | crate::gui::{GrinGui, Interaction, Message}, 5 | crate::localization::localized_string, 6 | crate::VERSION, 7 | grin_gui_core::theme::ColorPalette, 8 | grin_gui_core::theme::{ 9 | Button, Column, Container, Element, PickList, Row, Scrollable, Text, Theme, 10 | }, 11 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space, TextInput}, 12 | iced::{alignment, Alignment, Command, Length}, 13 | serde::{Deserialize, Serialize}, 14 | }; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct StateContainer { 18 | pub mode: Mode, 19 | } 20 | 21 | impl Default for StateContainer { 22 | fn default() -> Self { 23 | Self { mode: Mode::Wallet } 24 | } 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug, Clone)] 28 | pub enum LocalViewInteraction { 29 | SelectMode(Mode), 30 | } 31 | 32 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 33 | pub enum Mode { 34 | Wallet, 35 | Node, 36 | Settings, 37 | About, 38 | } 39 | 40 | pub fn handle_message( 41 | grin_gui: &mut GrinGui, 42 | message: LocalViewInteraction, 43 | ) -> crate::Result> { 44 | match message { 45 | LocalViewInteraction::SelectMode(mode) => { 46 | log::debug!("Interaction::ModeSelectedSettings({:?})", mode); 47 | // Set Mode 48 | grin_gui.menu_state.mode = mode 49 | } 50 | } 51 | Ok(Command::none()) 52 | } 53 | 54 | pub fn data_container<'a>( 55 | state: &'a StateContainer, 56 | error: &Option, 57 | ) -> Container<'a, Message> { 58 | let mut wallet_mode_button: Button = 59 | Button::new(Text::new(localized_string("wallet")).size(DEFAULT_FONT_SIZE)).on_press( 60 | Interaction::MenuViewInteraction(LocalViewInteraction::SelectMode(Mode::Wallet)), 61 | ); 62 | 63 | let mut node_mode_button: Button = 64 | Button::new(Text::new(localized_string("node")).size(DEFAULT_FONT_SIZE)).on_press( 65 | Interaction::MenuViewInteraction(LocalViewInteraction::SelectMode(Mode::Node)), 66 | ); 67 | 68 | let mut settings_mode_button: Button = Button::new( 69 | Text::new(localized_string("settings")) 70 | .horizontal_alignment(alignment::Horizontal::Center) 71 | .size(DEFAULT_FONT_SIZE), 72 | ) 73 | .on_press(Interaction::MenuViewInteraction( 74 | LocalViewInteraction::SelectMode(Mode::Settings), 75 | )); 76 | 77 | let mut about_mode_button: Button = Button::new( 78 | Text::new(localized_string("about")) 79 | .horizontal_alignment(alignment::Horizontal::Center) 80 | .size(DEFAULT_FONT_SIZE), 81 | ) 82 | .on_press(Interaction::MenuViewInteraction( 83 | LocalViewInteraction::SelectMode(Mode::About), 84 | )); 85 | 86 | match state.mode { 87 | Mode::Wallet => { 88 | wallet_mode_button = 89 | wallet_mode_button.style(grin_gui_core::theme::ButtonStyle::Selected); 90 | node_mode_button = node_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 91 | about_mode_button = about_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 92 | settings_mode_button = 93 | settings_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 94 | } 95 | Mode::Node => { 96 | wallet_mode_button = 97 | wallet_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 98 | node_mode_button = node_mode_button.style(grin_gui_core::theme::ButtonStyle::Selected); 99 | about_mode_button = about_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 100 | settings_mode_button = 101 | settings_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 102 | } 103 | Mode::Settings => { 104 | wallet_mode_button = 105 | wallet_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 106 | node_mode_button = node_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 107 | about_mode_button = about_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 108 | settings_mode_button = 109 | settings_mode_button.style(grin_gui_core::theme::ButtonStyle::Selected); 110 | } 111 | Mode::About => { 112 | wallet_mode_button = 113 | wallet_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 114 | node_mode_button = node_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 115 | about_mode_button = 116 | about_mode_button.style(grin_gui_core::theme::ButtonStyle::Selected); 117 | settings_mode_button = 118 | settings_mode_button.style(grin_gui_core::theme::ButtonStyle::Primary); 119 | } /*Mode::Setup => { 120 | wallet_mode_button = 121 | wallet_mode_button.style(style::DisabledDefaultButton); 122 | node_mode_button = node_mode_button.style(style::DisabledDefaultButton); 123 | about_mode_button = 124 | about_mode_button.style(style::DisabledDefaultButton); 125 | settings_mode_button = 126 | settings_mode_button.style(style::DisabledDefaultButton); 127 | }*/ 128 | } 129 | 130 | let wallet_mode_button: Element = wallet_mode_button.into(); 131 | let node_mode_button: Element = node_mode_button.into(); 132 | let settings_mode_button: Element = settings_mode_button.into(); 133 | let about_mode_button: Element = about_mode_button.into(); 134 | 135 | let segmented_addons_row = Row::with_children(vec![ 136 | wallet_mode_button.map(Message::Interaction), 137 | node_mode_button.map(Message::Interaction), 138 | ]) 139 | .spacing(1); 140 | 141 | /*let mut segmented_mode_row = Row::new().push(my_wallet_table_row).spacing(1); 142 | let segmented_mode_container = Container::new(segmented_mode_row) 143 | .padding(2) 144 | .style(grin_gui_core::theme::container::Container::Segmented);*/ 145 | 146 | let segmented_addon_container = Container::new(segmented_addons_row) 147 | .padding(2) 148 | .style(grin_gui_core::theme::ContainerStyle::Segmented); 149 | 150 | // Empty container shown if no error message 151 | let mut error_column = Column::new(); 152 | 153 | if let Some(e) = error { 154 | // Displays an error + detail button, if any has occured. 155 | 156 | let mut error_string = e.to_string(); 157 | let mut causes = e.chain(); 158 | 159 | let count = causes.clone().count(); 160 | let top_level_cause = causes.next(); 161 | 162 | if count > 1 { 163 | error_string = format!("{} - {}", error_string, causes.next().unwrap()); 164 | } 165 | 166 | let error_text = Text::new(error_string).size(DEFAULT_FONT_SIZE); 167 | 168 | let error_detail_button: Button = Button::new( 169 | Text::new(localized_string("more-error-detail")) 170 | .horizontal_alignment(alignment::Horizontal::Center) 171 | .vertical_alignment(alignment::Vertical::Center) 172 | .size(SMALLER_FONT_SIZE), 173 | ) 174 | .style(grin_gui_core::theme::ButtonStyle::NormalText) 175 | .on_press(Interaction::OpenErrorModal); 176 | 177 | let error_detail_button: Element = error_detail_button.into(); 178 | 179 | error_column = Column::with_children(vec![ 180 | Space::with_height(Length::Fixed(5.0)).into(), 181 | error_text.into(), 182 | Space::with_height(Length::Fixed(5.0)).into(), 183 | error_detail_button.map(Message::Interaction), 184 | ]) 185 | .align_items(Alignment::Center); 186 | } 187 | 188 | let error_container: Container = Container::new(error_column) 189 | .center_y() 190 | .center_x() 191 | .width(Length::Fill) 192 | .style(grin_gui_core::theme::ContainerStyle::ErrorForeground); 193 | 194 | /*let version_text = Text::new(if let Some(release) = &self_update_state.latest_release { 195 | if VersionCompare::compare_to(&release.tag_name, VERSION, &CompOp::Gt).unwrap_or(false) { 196 | if is_updatable { 197 | needs_update = true; 198 | } 199 | 200 | format!( 201 | "{} {} -> {}", 202 | localized_string("new-update-available"), 203 | VERSION, 204 | &release.tag_name 205 | ) 206 | } else { 207 | VERSION.to_owned() 208 | } 209 | } else { 210 | VERSION.to_owned() 211 | })*/ 212 | 213 | let version_text = Text::new(VERSION.to_owned()) 214 | .size(DEFAULT_FONT_SIZE) 215 | .horizontal_alignment(alignment::Horizontal::Right); 216 | 217 | let version_container = Container::new(version_text) 218 | .center_y() 219 | .padding(5) 220 | .style(grin_gui_core::theme::ContainerStyle::BrightForeground); 221 | 222 | let segmented_mode_control_row: Row = Row::with_children(vec![ 223 | about_mode_button.map(Message::Interaction), 224 | settings_mode_button.map(Message::Interaction), 225 | ]) 226 | .spacing(1); 227 | 228 | let segmented_mode_control_container = Container::new(segmented_mode_control_row) 229 | .padding(2) 230 | .style(grin_gui_core::theme::ContainerStyle::Segmented); 231 | 232 | let settings_row = Row::with_children(vec![ 233 | segmented_addon_container.into(), 234 | Space::with_width(Length::Fixed(DEFAULT_PADDING)).into(), 235 | error_container.into(), 236 | version_container.into(), 237 | segmented_mode_control_container.into(), 238 | ]) 239 | .align_items(Alignment::Center); 240 | 241 | // Wraps it in a container with even padding on all sides 242 | Container::new(settings_row) 243 | .style(grin_gui_core::theme::ContainerStyle::BrightForeground) 244 | .padding(iced::Padding::from([ 245 | DEFAULT_PADDING, // top 246 | DEFAULT_PADDING, // right 247 | DEFAULT_PADDING, // bottom 248 | DEFAULT_PADDING, // left 249 | ])) 250 | } 251 | -------------------------------------------------------------------------------- /src/gui/element/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod about; 2 | pub mod menu; 3 | pub mod modal; 4 | pub mod node; 5 | pub mod settings; 6 | pub mod wallet; 7 | 8 | // Default values used on multiple elements. 9 | pub static SMALLER_FONT_SIZE: u16 = 12; 10 | pub static DEFAULT_FONT_SIZE: u16 = 14; 11 | pub static DEFAULT_HEADER_FONT_SIZE: u16 = 24; 12 | pub static DEFAULT_SUB_HEADER_FONT_SIZE: u16 = 18; 13 | pub static DEFAULT_PADDING: f32 = 10.0; 14 | 15 | pub static BUTTON_WIDTH: f32 = 84.0; 16 | pub static BUTTON_HEIGHT: f32 = 20.0; 17 | -------------------------------------------------------------------------------- /src/gui/element/modal.rs: -------------------------------------------------------------------------------- 1 | use iced::Renderer; 2 | 3 | use super::{BUTTON_HEIGHT, BUTTON_WIDTH}; 4 | 5 | use { 6 | super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, SMALLER_FONT_SIZE}, 7 | crate::gui::{Interaction, Message}, 8 | crate::localization::localized_string, 9 | grin_gui_core::theme::ColorPalette, 10 | grin_gui_core::theme::{ 11 | Button, Card, Column, Container, Element, PickList, Row, Scrollable, Text, 12 | }, 13 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space, TextInput}, 14 | iced::{alignment, Alignment, Command, Length}, 15 | iced_aw::{modal, Modal}, 16 | }; 17 | 18 | pub struct StateContainer {} 19 | 20 | impl Default for StateContainer { 21 | fn default() -> Self { 22 | Self {} 23 | } 24 | } 25 | 26 | pub fn exit_card() -> Card<'static, Message> { 27 | let button_height = Length::Fixed(BUTTON_HEIGHT); 28 | let button_width = Length::Fixed(BUTTON_WIDTH); 29 | 30 | let yes_button_label = 31 | Container::new(Text::new(localized_string("yes")).size(DEFAULT_FONT_SIZE)) 32 | .width(button_width) 33 | .height(button_height) 34 | .center_x() 35 | .center_y() 36 | .align_x(alignment::Horizontal::Center); 37 | 38 | let cancel_button_label = 39 | Container::new(Text::new(localized_string("no")).size(DEFAULT_FONT_SIZE)) 40 | .width(button_width) 41 | .height(button_height) 42 | .center_x() 43 | .center_y() 44 | .align_x(alignment::Horizontal::Center); 45 | 46 | let yes_button: Element = Button::new(yes_button_label) 47 | .style(grin_gui_core::theme::ButtonStyle::Primary) 48 | .on_press(Interaction::Exit) 49 | .into(); 50 | 51 | let cancel_button: Element = Button::new(cancel_button_label) 52 | .style(grin_gui_core::theme::ButtonStyle::Primary) 53 | .on_press(Interaction::ExitCancel) 54 | .into(); 55 | 56 | let unit_spacing = 15.0; 57 | 58 | // button lipstick 59 | let yes_container = Container::new(yes_button.map(Message::Interaction)).padding(1); 60 | let yes_container = Container::new(yes_container) 61 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 62 | .padding(1); 63 | let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); 64 | let cancel_container = Container::new(cancel_container) 65 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 66 | .padding(1); 67 | 68 | let button_row = Row::new() 69 | .push(yes_container) 70 | .push(Space::new(Length::Fixed(unit_spacing), Length::Fixed(0.0))) 71 | .push(cancel_container); 72 | 73 | Card::new( 74 | Text::new(localized_string("exit-confirm-title")) 75 | .size(DEFAULT_HEADER_FONT_SIZE) 76 | .horizontal_alignment(alignment::Horizontal::Center), 77 | Text::new(localized_string("exit-confirm-msg")).size(DEFAULT_FONT_SIZE), 78 | ) 79 | .foot( 80 | Column::new() 81 | .spacing(10) 82 | .padding(5) 83 | .width(Length::Fill) 84 | .align_items(Alignment::Center) 85 | .push(button_row), 86 | ) 87 | .max_width(500.0) 88 | .on_close(Message::Interaction(Interaction::ExitCancel)) 89 | .style(grin_gui_core::theme::CardStyle::Normal) 90 | } 91 | 92 | pub fn error_card(error_cause: String) -> Card<'static, Message> { 93 | Card::new( 94 | Text::new(localized_string("error-detail")).size(DEFAULT_HEADER_FONT_SIZE), 95 | Text::new(error_cause.clone()).size(DEFAULT_FONT_SIZE), 96 | ) 97 | .foot( 98 | Column::new() 99 | .spacing(10) 100 | .padding(5) 101 | .width(Length::Fill) 102 | .align_items(Alignment::Center) 103 | .push( 104 | Button::new( 105 | Text::new(localized_string("ok-caps")) 106 | .size(DEFAULT_FONT_SIZE) 107 | .horizontal_alignment(alignment::Horizontal::Center), 108 | ) 109 | .style(grin_gui_core::theme::ButtonStyle::Primary) 110 | .on_press(Message::Interaction(Interaction::CloseErrorModal)), 111 | ) 112 | .push( 113 | Button::new( 114 | Text::new(localized_string("copy-to-clipboard")) 115 | .size(SMALLER_FONT_SIZE) 116 | .horizontal_alignment(alignment::Horizontal::Center), 117 | ) 118 | .style(grin_gui_core::theme::ButtonStyle::NormalText) 119 | .on_press(Message::Interaction(Interaction::WriteToClipboard( 120 | error_cause, 121 | ))), 122 | ), 123 | ) 124 | .max_width(500.0) 125 | .on_close(Message::Interaction(Interaction::CloseErrorModal)) 126 | .style(grin_gui_core::theme::CardStyle::Normal) 127 | } 128 | -------------------------------------------------------------------------------- /src/gui/element/node/embedded/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::gui::element::DEFAULT_PADDING; 2 | use iced_style::container::StyleSheet; 3 | 4 | pub mod summary; 5 | 6 | use { 7 | crate::gui::{GrinGui, Message}, 8 | crate::Result, 9 | grin_gui_core::node::ChainTypes, 10 | grin_gui_core::node::ServerStats, 11 | grin_gui_core::theme::ColorPalette, 12 | grin_gui_core::theme::{Column, Container, Element}, 13 | iced::widget::container, 14 | iced::Command, 15 | iced_core::Length, 16 | }; 17 | 18 | pub struct StateContainer { 19 | pub mode: Mode, 20 | pub server_stats: Option, 21 | pub summary_state: summary::StateContainer, 22 | } 23 | 24 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 25 | pub enum Mode { 26 | Summary, 27 | 28 | Peers, 29 | // etc as in TUI 30 | } 31 | 32 | impl Default for StateContainer { 33 | fn default() -> Self { 34 | Self { 35 | mode: Mode::Summary, 36 | server_stats: None, 37 | summary_state: Default::default(), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub enum LocalViewInteraction {} 44 | 45 | pub fn handle_message( 46 | grin_gui: &mut GrinGui, 47 | message: LocalViewInteraction, 48 | ) -> Result> { 49 | Ok(Command::none()) 50 | } 51 | 52 | pub fn data_container<'a>( 53 | state: &'a StateContainer, 54 | chain_type: ChainTypes, 55 | ) -> Container<'a, Message> { 56 | let content = match state.mode { 57 | Mode::Summary => { 58 | summary::data_container(&state.summary_state, &state.server_stats, chain_type) 59 | } 60 | _ => Container::new(Column::new()).into(), 61 | }; 62 | 63 | let column = Column::new().push(content); 64 | 65 | Container::new(column) 66 | .center_y() 67 | .center_x() 68 | .width(Length::Fill) 69 | .height(Length::Fill) 70 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 71 | .padding(iced::Padding { 72 | top: DEFAULT_PADDING, // top 73 | right: DEFAULT_PADDING, // right 74 | bottom: DEFAULT_PADDING, // bottom 75 | left: DEFAULT_PADDING, // left 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /src/gui/element/node/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod embedded; 2 | 3 | use { 4 | crate::gui::Message, 5 | grin_gui_core::theme::{Column, Container}, 6 | grin_gui_core::{ 7 | node::ChainTypes, 8 | theme::{ColorPalette, Element}, 9 | }, 10 | iced::Length, 11 | }; 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 14 | pub enum Mode { 15 | Embedded, 16 | // etc, as in TUI for now 17 | } 18 | 19 | pub struct StateContainer { 20 | pub mode: Mode, 21 | //pub external_state: external::StateContainer, 22 | pub embedded_state: embedded::StateContainer, 23 | } 24 | 25 | impl Default for StateContainer { 26 | fn default() -> Self { 27 | Self { 28 | mode: Mode::Embedded, 29 | embedded_state: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | impl StateContainer {} 35 | 36 | pub fn data_container<'a>( 37 | state: &'a StateContainer, 38 | chain_type: ChainTypes, 39 | ) -> Container<'a, Message> { 40 | let content = match state.mode { 41 | Mode::Embedded => embedded::data_container(&state.embedded_state, chain_type), 42 | //_ => Container::new(Column::new()), 43 | }; 44 | 45 | let column = Column::new() 46 | //.push(Space::new(Length::Fixed(0.0), Length::Fixed(20))) 47 | .push(content); 48 | 49 | Container::new(column) 50 | .center_y() 51 | .center_x() 52 | .width(Length::Fill) 53 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 54 | } 55 | -------------------------------------------------------------------------------- /src/gui/element/settings/mod.rs: -------------------------------------------------------------------------------- 1 | use iced::alignment::Horizontal; 2 | 3 | pub mod general; 4 | pub mod node; 5 | pub mod wallet; 6 | 7 | use { 8 | super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING}, 9 | crate::gui::{GrinGui, Interaction, Message}, 10 | crate::localization::localized_string, 11 | grin_gui_core::theme::{ 12 | Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 13 | }, 14 | grin_gui_core::{config::Config, theme::ColorPalette}, 15 | iced::widget::{button, Space}, 16 | iced::{Alignment, Length}, 17 | serde::{Deserialize, Serialize}, 18 | }; 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct StateContainer { 22 | pub mode: Mode, 23 | // wallet_btn: button::State, 24 | // node_btn: button::State, 25 | // general_btn: button::State, 26 | } 27 | 28 | impl Default for StateContainer { 29 | fn default() -> Self { 30 | Self { 31 | mode: Mode::Wallet, 32 | // wallet_btn: Default::default(), 33 | // node_btn: Default::default(), 34 | // general_btn: Default::default(), 35 | } 36 | } 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug, Clone)] 40 | pub enum LocalViewInteraction { 41 | SelectMode(Mode), 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug, Clone)] 45 | pub enum Mode { 46 | Wallet, 47 | Node, 48 | General, 49 | } 50 | 51 | pub fn handle_message(grin_gui: &mut GrinGui, message: LocalViewInteraction) { 52 | match message { 53 | LocalViewInteraction::SelectMode(mode) => { 54 | log::debug!("Interaction::ModeSelectedSettings({:?})", mode); 55 | // Set Mode 56 | grin_gui.settings_state.mode = mode; 57 | } 58 | } 59 | } 60 | 61 | pub fn data_container<'a>( 62 | state: &'a StateContainer, 63 | config: &'a Config, 64 | wallet_settings_state: &'a wallet::StateContainer, 65 | node_settings_state: &'a node::StateContainer, 66 | general_settings_state: &'a general::StateContainer, 67 | ) -> Container<'a, Message> { 68 | let title_string = match state.mode { 69 | Mode::Wallet => localized_string("settings-wallet"), 70 | Mode::Node => localized_string("settings-node"), 71 | Mode::General => localized_string("settings-general"), 72 | }; 73 | 74 | // Submenu title to appear of left side of panel 75 | let general_settings_title = Text::new(title_string).size(DEFAULT_HEADER_FONT_SIZE); 76 | let general_settings_title_container = Container::new(general_settings_title) 77 | .style(grin_gui_core::theme::ContainerStyle::BrightBackground) 78 | .padding(iced::Padding::from([ 79 | 0, // top 80 | 0, // right 81 | 0, // bottom 82 | 5, // left 83 | ])); 84 | 85 | let mut wallet_button: Button = Button::new( 86 | // &mut state.wallet_btn, 87 | Text::new(localized_string("wallet")).size(DEFAULT_FONT_SIZE), 88 | ) 89 | .on_press(Interaction::SettingsViewInteraction( 90 | LocalViewInteraction::SelectMode(Mode::Wallet), 91 | )); 92 | 93 | let mut node_button: Button = Button::new( 94 | // &mut state.node_btn, 95 | Text::new(localized_string("node")).size(DEFAULT_FONT_SIZE), 96 | ) 97 | .on_press(Interaction::SettingsViewInteraction( 98 | LocalViewInteraction::SelectMode(Mode::Node), 99 | )); 100 | 101 | let mut general_button: Button = Button::new( 102 | // &mut state.general_btn, 103 | Text::new(localized_string("general")).size(DEFAULT_FONT_SIZE), 104 | ) 105 | .on_press(Interaction::SettingsViewInteraction( 106 | LocalViewInteraction::SelectMode(Mode::General), 107 | )); 108 | 109 | match state.mode { 110 | Mode::Wallet => { 111 | wallet_button = wallet_button.style(grin_gui_core::theme::ButtonStyle::Selected); 112 | node_button = node_button.style(grin_gui_core::theme::ButtonStyle::Primary); 113 | general_button = general_button.style(grin_gui_core::theme::ButtonStyle::Primary); 114 | } 115 | Mode::Node => { 116 | wallet_button = wallet_button.style(grin_gui_core::theme::ButtonStyle::Primary); 117 | node_button = node_button.style(grin_gui_core::theme::ButtonStyle::Selected); 118 | general_button = general_button.style(grin_gui_core::theme::ButtonStyle::Primary); 119 | } 120 | Mode::General => { 121 | wallet_button = wallet_button.style(grin_gui_core::theme::ButtonStyle::Primary); 122 | node_button = node_button.style(grin_gui_core::theme::ButtonStyle::Primary); 123 | general_button = general_button.style(grin_gui_core::theme::ButtonStyle::Selected); 124 | } 125 | } 126 | 127 | let wallet_button: Element = wallet_button.into(); 128 | let node_button: Element = node_button.into(); 129 | let general_button: Element = general_button.into(); 130 | 131 | let segmented_mode_row = Row::new() 132 | .push(wallet_button.map(Message::Interaction)) 133 | .push(node_button.map(Message::Interaction)) 134 | .push(general_button.map(Message::Interaction)) 135 | .spacing(1); 136 | 137 | let segmented_mode_container = Container::new(segmented_mode_row).padding(1); 138 | 139 | let segmented_mode_control_container = Container::new(segmented_mode_container) 140 | .padding(1) 141 | .style(grin_gui_core::theme::ContainerStyle::Segmented); 142 | 143 | let header_row = Row::new() 144 | .push(general_settings_title_container) 145 | .push(Space::with_width(Length::Fill)) 146 | .push(segmented_mode_control_container) 147 | .align_items(Alignment::Center); 148 | 149 | let header_container = Container::new(header_row); 150 | 151 | // Wrapper for submenu + actual content 152 | let mut wrapper_column = 153 | Column::with_children(vec![header_container.into()]).height(Length::Fill); 154 | // Submenu Area + actual content 155 | match state.mode { 156 | Mode::Wallet => { 157 | wrapper_column = 158 | wrapper_column.push(wallet::data_container(wallet_settings_state, config)) 159 | } 160 | Mode::Node => { 161 | wrapper_column = wrapper_column.push(node::data_container(node_settings_state)) 162 | } 163 | Mode::General => { 164 | wrapper_column = 165 | wrapper_column.push(general::data_container(general_settings_state, config)) 166 | } 167 | } 168 | 169 | Container::new(wrapper_column) 170 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 171 | .padding(iced::Padding::from([ 172 | DEFAULT_PADDING, // top 173 | DEFAULT_PADDING, // right 174 | DEFAULT_PADDING, // bottom 175 | DEFAULT_PADDING, // left 176 | ])) 177 | } 178 | -------------------------------------------------------------------------------- /src/gui/element/settings/node.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::DEFAULT_FONT_SIZE, 3 | crate::gui::{GrinGui, Message}, 4 | crate::localization::localized_string, 5 | grin_gui_core::theme::ColorPalette, 6 | grin_gui_core::theme::{Button, Column, Container, PickList, Row, Scrollable, Text, TextInput}, 7 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, 8 | iced::Length, 9 | serde::{Deserialize, Serialize}, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct StateContainer { 14 | pub mode: Mode, 15 | // scrollable_state: scrollable::State, 16 | } 17 | 18 | impl Default for StateContainer { 19 | fn default() -> Self { 20 | Self { 21 | mode: Mode::Wallet, 22 | // scrollable_state: Default::default(), 23 | } 24 | } 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug, Clone)] 28 | pub enum LocalViewInteraction { 29 | SelectMode(Mode), 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug, Clone)] 33 | pub enum Mode { 34 | Wallet, 35 | Node, 36 | General, 37 | } 38 | 39 | pub fn handle_message(grin_gui: &mut GrinGui, message: LocalViewInteraction) { 40 | match message { 41 | LocalViewInteraction::SelectMode(mode) => { 42 | log::debug!("Interaction::ModeSelectedSettings({:?})", mode); 43 | // Set Mode 44 | grin_gui.node_settings_state.mode = mode 45 | } 46 | } 47 | } 48 | 49 | pub fn data_container<'a>(state: &'a StateContainer) -> Container<'a, Message> { 50 | let language_container = { 51 | let title = Container::new(Text::new(localized_string("todo")).size(DEFAULT_FONT_SIZE)) 52 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 53 | 54 | Column::new() 55 | .push(title) 56 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 57 | }; 58 | 59 | // Colum wrapping all the settings content. 60 | let scrollable = Scrollable::new(language_container) 61 | .height(Length::Fill) 62 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 63 | 64 | let col = Column::new() 65 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(10.0))) 66 | .push(scrollable) 67 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(20.0))); 68 | let row = Row::new() 69 | .push(Space::new(Length::Fixed(5.0), Length::Fixed(0.0))) 70 | .push(col); 71 | 72 | // Returns the final container. 73 | Container::new(row) 74 | .width(Length::Fill) 75 | .height(Length::Shrink) 76 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 77 | } 78 | -------------------------------------------------------------------------------- /src/gui/element/settings/wallet.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::DEFAULT_FONT_SIZE, 3 | crate::gui::{GrinGui, Interaction, Message}, 4 | crate::localization::localized_string, 5 | grin_gui_core::config::{Config, TxMethod}, 6 | grin_gui_core::fs::PersistentData, 7 | grin_gui_core::theme::{ 8 | Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 9 | }, 10 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, 11 | iced::Length, 12 | iced::{alignment, Alignment}, 13 | serde::{Deserialize, Serialize}, 14 | }; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct StateContainer { 18 | // scrollable_state: scrollable::State, 19 | mw_mixnet_address_1: String, 20 | mw_mixnet_address_2: String, 21 | mw_mixnet_address_3: String, 22 | } 23 | 24 | impl Default for StateContainer { 25 | fn default() -> Self { 26 | Self { 27 | mw_mixnet_address_1: "".to_string(), 28 | mw_mixnet_address_2: "".to_string(), 29 | mw_mixnet_address_3: "".to_string(), 30 | } 31 | } 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Debug, Clone)] 35 | pub enum LocalViewInteraction { 36 | TxMethodSelected(TxMethod), 37 | MwMixnetAddress1Changed(String), 38 | MwMixnetAddress2Changed(String), 39 | MwMixnetAddress3Changed(String), 40 | } 41 | 42 | #[derive(Serialize, Deserialize, Debug, Clone)] 43 | pub enum Mode { 44 | Wallet, 45 | Node, 46 | General, 47 | } 48 | 49 | pub fn handle_message(grin_gui: &mut GrinGui, message: LocalViewInteraction) { 50 | let state = &mut grin_gui.wallet_settings_state; 51 | let mut check_mixnet_config = || { 52 | if grin_gui.config.mixnet_keys.is_none() { 53 | grin_gui.config.mixnet_keys = Some(vec![]); 54 | grin_gui 55 | .config 56 | .mixnet_keys 57 | .as_mut() 58 | .unwrap() 59 | .push(String::new()); 60 | grin_gui 61 | .config 62 | .mixnet_keys 63 | .as_mut() 64 | .unwrap() 65 | .push(String::new()); 66 | grin_gui 67 | .config 68 | .mixnet_keys 69 | .as_mut() 70 | .unwrap() 71 | .push(String::new()); 72 | } 73 | }; 74 | match message { 75 | LocalViewInteraction::TxMethodSelected(method) => { 76 | log::debug!("Interaction::TxMethodSelectedSettings({:?})", method); 77 | // Set Mode 78 | grin_gui.config.tx_method = method; 79 | let _ = grin_gui.config.save(); 80 | } 81 | LocalViewInteraction::MwMixnetAddress1Changed(value) => { 82 | check_mixnet_config(); 83 | grin_gui.config.mixnet_keys.as_mut().unwrap()[0] = value.clone(); 84 | state.mw_mixnet_address_1 = value; 85 | let _ = grin_gui.config.save(); 86 | } 87 | LocalViewInteraction::MwMixnetAddress2Changed(value) => { 88 | check_mixnet_config(); 89 | grin_gui.config.mixnet_keys.as_mut().unwrap()[1] = value.clone(); 90 | state.mw_mixnet_address_2 = value; 91 | let _ = grin_gui.config.save(); 92 | } 93 | LocalViewInteraction::MwMixnetAddress3Changed(value) => { 94 | check_mixnet_config(); 95 | grin_gui.config.mixnet_keys.as_mut().unwrap()[2] = value.clone(); 96 | state.mw_mixnet_address_3 = value; 97 | let _ = grin_gui.config.save(); 98 | } 99 | } 100 | } 101 | 102 | pub fn data_container<'a>(state: &'a StateContainer, config: &Config) -> Container<'a, Message> { 103 | let (config_addr_1, config_addr_2, config_addr_3) = if let Some(a) = config.mixnet_keys.as_ref() 104 | { 105 | (a[0].clone(), a[1].clone(), a[2].clone()) 106 | } else { 107 | (String::new(), String::new(), String::new()) 108 | }; 109 | 110 | let tx_method_column = { 111 | let tx_method_container = 112 | Container::new(Text::new(localized_string("tx-method")).size(DEFAULT_FONT_SIZE)) 113 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 114 | 115 | let tx_method_pick_list = PickList::new(&TxMethod::ALL[..], Some(config.tx_method), |t| { 116 | Message::Interaction(Interaction::WalletSettingsViewInteraction( 117 | LocalViewInteraction::TxMethodSelected(t), 118 | )) 119 | }) 120 | .text_size(DEFAULT_FONT_SIZE) 121 | .width(Length::Fixed(120.0)) 122 | .style(grin_gui_core::theme::PickListStyle::Primary); 123 | 124 | // Data row for theme picker list. 125 | let tx_method_data_row = Row::new() 126 | .push(tx_method_pick_list) 127 | .align_items(Alignment::Center) 128 | .height(Length::Fixed(26.0)); 129 | 130 | Column::new() 131 | .push(tx_method_container) 132 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 133 | .push(tx_method_data_row) 134 | }; 135 | 136 | let mw_mixnet_address_column = { 137 | let mw_mixnet_address_container = Container::new( 138 | Text::new(localized_string("mw-mixnet-addresses")).size(DEFAULT_FONT_SIZE), 139 | ) 140 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 141 | 142 | let mw_mixnet_address_1 = Text::new(localized_string("mw-mixnet-address-1")) 143 | .size(DEFAULT_FONT_SIZE) 144 | .horizontal_alignment(alignment::Horizontal::Left); 145 | 146 | let mw_mixnet_address_1_container = Container::new(mw_mixnet_address_1) 147 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 148 | 149 | let mw_mixnet_address_1_input = TextInput::new("", &config_addr_1) 150 | .on_input(|s| { 151 | Interaction::WalletSettingsViewInteraction( 152 | LocalViewInteraction::MwMixnetAddress1Changed(s), 153 | ) 154 | }) 155 | .size(DEFAULT_FONT_SIZE) 156 | .padding(6) 157 | .width(Length::Fixed(400.0)) 158 | .style(grin_gui_core::theme::TextInputStyle::AddonsQuery); 159 | 160 | let mw_mixnet_address_1_input: Element = mw_mixnet_address_1_input.into(); 161 | 162 | let mw_mixnet_address_2 = Text::new(localized_string("mw-mixnet-address-2")) 163 | .size(DEFAULT_FONT_SIZE) 164 | .horizontal_alignment(alignment::Horizontal::Left); 165 | 166 | let mw_mixnet_address_2_container = Container::new(mw_mixnet_address_2) 167 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 168 | 169 | let mw_mixnet_address_2_input = TextInput::new("", &config_addr_2) 170 | .on_input(|s| { 171 | Interaction::WalletSettingsViewInteraction( 172 | LocalViewInteraction::MwMixnetAddress2Changed(s), 173 | ) 174 | }) 175 | .size(DEFAULT_FONT_SIZE) 176 | .padding(6) 177 | .width(Length::Fixed(400.0)) 178 | .style(grin_gui_core::theme::TextInputStyle::AddonsQuery); 179 | 180 | let mw_mixnet_address_2_input: Element = mw_mixnet_address_2_input.into(); 181 | 182 | let mw_mixnet_address_3 = Text::new(localized_string("mw-mixnet-address-3")) 183 | .size(DEFAULT_FONT_SIZE) 184 | .horizontal_alignment(alignment::Horizontal::Left); 185 | 186 | let mw_mixnet_address_3_container = Container::new(mw_mixnet_address_3) 187 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 188 | 189 | let mw_mixnet_address_3_input = TextInput::new("", &config_addr_3) 190 | .on_input(|s| { 191 | Interaction::WalletSettingsViewInteraction( 192 | LocalViewInteraction::MwMixnetAddress3Changed(s), 193 | ) 194 | }) 195 | .size(DEFAULT_FONT_SIZE) 196 | .padding(6) 197 | .width(Length::Fixed(400.0)) 198 | .style(grin_gui_core::theme::TextInputStyle::AddonsQuery); 199 | 200 | let mw_mixnet_address_3_input: Element = mw_mixnet_address_3_input.into(); 201 | 202 | // Data row for theme picker list. 203 | /*let tx_method_data_row = Row::new() 204 | .push(tx_method_pick_list) 205 | .align_items(Alignment::Center) 206 | .height(Length::Fixed(26.0));*/ 207 | 208 | Column::new() 209 | .push(mw_mixnet_address_container) 210 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 211 | .push(mw_mixnet_address_1_container) 212 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 213 | .push(mw_mixnet_address_1_input.map(Message::Interaction)) 214 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 215 | .push(mw_mixnet_address_2_container) 216 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 217 | .push(mw_mixnet_address_2_input.map(Message::Interaction)) 218 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 219 | .push(mw_mixnet_address_3_container) 220 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))) 221 | .push(mw_mixnet_address_3_input.map(Message::Interaction)) 222 | }; 223 | 224 | let wrap = { 225 | Column::new() 226 | .push(tx_method_column) 227 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(10.0))) 228 | .push(mw_mixnet_address_column) 229 | }; 230 | 231 | let scrollable = Scrollable::new(wrap) 232 | .height(Length::Fill) 233 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 234 | 235 | let col = Column::new() 236 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(10.0))) 237 | .push(scrollable) 238 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(20.0))); 239 | let row = Row::new() 240 | .push(Space::new(Length::Fixed(5.0), Length::Fixed(0.0))) 241 | .push(col); 242 | 243 | // Returns the final container. 244 | Container::new(row) 245 | .width(Length::Fill) 246 | .height(Length::Shrink) 247 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 248 | } 249 | -------------------------------------------------------------------------------- /src/gui/element/wallet/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod operation; 2 | pub mod setup; 3 | 4 | use { 5 | crate::gui::Message, 6 | grin_gui_core::config::Config, 7 | grin_gui_core::theme::ColorPalette, 8 | grin_gui_core::theme::{Column, Container, Element, Theme}, 9 | iced::Length, 10 | }; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 | pub enum Mode { 14 | Init, 15 | CreateWallet(String), 16 | ImportWallet, 17 | Operation, 18 | } 19 | 20 | pub struct StateContainer { 21 | pub mode: Mode, 22 | pub setup_state: setup::StateContainer, 23 | pub operation_state: operation::StateContainer, 24 | // When changed to true, this should stay false until a config exists 25 | has_config_check_failed_one_time: bool, 26 | } 27 | 28 | impl Default for StateContainer { 29 | fn default() -> Self { 30 | Self { 31 | mode: Mode::Operation, 32 | setup_state: Default::default(), 33 | operation_state: Default::default(), 34 | has_config_check_failed_one_time: false, 35 | } 36 | } 37 | } 38 | 39 | impl StateContainer { 40 | pub fn config_missing(&self) -> bool { 41 | self.has_config_check_failed_one_time 42 | } 43 | 44 | pub fn set_config_missing(&mut self) { 45 | self.has_config_check_failed_one_time = true; 46 | self.mode = Mode::Init; 47 | self.setup_state.mode = crate::gui::element::wallet::setup::Mode::Init; 48 | } 49 | 50 | pub fn clear_config_missing(&mut self) { 51 | self.has_config_check_failed_one_time = false; 52 | } 53 | } 54 | 55 | pub fn data_container<'a>(state: &'a StateContainer, config: &'a Config) -> Container<'a, Message> { 56 | let content = match &state.mode { 57 | Mode::Init => setup::data_container(&state.setup_state, config), 58 | Mode::Operation => operation::data_container(&state.operation_state, config), 59 | Mode::CreateWallet(default_display_name) => setup::wallet_setup::data_container( 60 | &state.setup_state.setup_wallet_state, 61 | default_display_name, 62 | ), 63 | Mode::ImportWallet => { 64 | setup::wallet_import::data_container(config, &state.setup_state.import_wallet_state) 65 | } 66 | }; 67 | 68 | let column = Column::new() 69 | //.push(Space::new(Length::Fixed(0.0), Length::Fixed(20))) 70 | .push(content); 71 | 72 | Container::new(column) 73 | .center_y() 74 | .center_x() 75 | .width(Length::Fill) 76 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 77 | } 78 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/action_menu.rs: -------------------------------------------------------------------------------- 1 | use super::tx_list::{self, ExpandType}; 2 | use crate::log_error; 3 | use async_std::prelude::FutureExt; 4 | use grin_gui_core::{ 5 | config::Config, 6 | wallet::{TxLogEntry, TxLogEntryType}, 7 | }; 8 | //use grin_gui_widgets::{header}; 9 | //use grin_gui_core::widgets::widget::header; 10 | use iced::alignment; 11 | use iced_aw::Card; 12 | use iced_core::Widget; 13 | use std::path::PathBuf; 14 | 15 | use super::tx_list::{HeaderState, TxList}; 16 | 17 | use { 18 | super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING}, 19 | crate::gui::{GrinGui, Interaction, Message}, 20 | crate::localization::localized_string, 21 | crate::Result, 22 | anyhow::Context, 23 | grin_gui_core::theme::{ 24 | Button, Column, Container, Element, PickList, Row, Scrollable, TableRow, Text, TextInput, 25 | }, 26 | grin_gui_core::wallet::{StatusMessage, WalletInfo, WalletInterface}, 27 | grin_gui_core::{node::amount_to_hr_string, theme::ColorPalette}, 28 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, 29 | iced::{Alignment, Command, Length}, 30 | serde::{Deserialize, Serialize}, 31 | std::sync::{Arc, RwLock}, 32 | }; 33 | 34 | pub struct StateContainer { 35 | // pub create_tx_button_state: button::State, 36 | // pub apply_tx_button_state: button::State, 37 | } 38 | 39 | impl Default for StateContainer { 40 | fn default() -> Self { 41 | Self { 42 | // create_tx_button_state: Default::default(), 43 | // apply_tx_button_state: Default::default(), 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 49 | pub enum Action { 50 | CreateTx, 51 | ApplyTx, 52 | } 53 | 54 | #[derive(Debug, Clone)] 55 | pub enum LocalViewInteraction { 56 | SelectAction(Action), 57 | } 58 | 59 | pub fn handle_message<'a>( 60 | grin_gui: &mut GrinGui, 61 | message: LocalViewInteraction, 62 | ) -> Result> { 63 | let state = &mut grin_gui 64 | .wallet_state 65 | .operation_state 66 | .home_state 67 | .action_menu_state; 68 | match message { 69 | LocalViewInteraction::SelectAction(action) => { 70 | log::debug!( 71 | "Interaction::WalletOperationHomeActionMenuViewInteraction({:?})", 72 | action 73 | ); 74 | match action { 75 | Action::CreateTx => { 76 | grin_gui.wallet_state.operation_state.mode = 77 | crate::gui::element::wallet::operation::Mode::CreateTx 78 | } 79 | Action::ApplyTx => { 80 | grin_gui.wallet_state.operation_state.mode = 81 | crate::gui::element::wallet::operation::Mode::ApplyTx 82 | } 83 | } 84 | } 85 | } 86 | Ok(Command::none()) 87 | } 88 | 89 | pub fn data_container<'a>( 90 | config: &'a Config, 91 | state: &'a StateContainer, 92 | home_state: &'a super::home::StateContainer, 93 | ) -> Container<'a, Message> { 94 | let button_width = Length::Fixed(70.0); 95 | 96 | let description = Text::new(localized_string("tx-transact")) 97 | .size(DEFAULT_FONT_SIZE) 98 | .horizontal_alignment(alignment::Horizontal::Center) 99 | .vertical_alignment(alignment::Vertical::Center); 100 | let description_container = Container::new(description).padding(iced::Padding::from([ 101 | 7, // top 102 | 5, // right 103 | 5, // bottom 104 | 5, // left 105 | ])); 106 | 107 | // Buttons to perform wallet operations 108 | let create_tx_container = 109 | Container::new(Text::new(localized_string("wallet-create-tx")).size(DEFAULT_FONT_SIZE)) 110 | .width(button_width) 111 | .align_y(alignment::Vertical::Center) 112 | .align_x(alignment::Horizontal::Center); 113 | 114 | let mut create_tx_button = Button::new(create_tx_container) 115 | .width(button_width) 116 | .style(grin_gui_core::theme::ButtonStyle::Primary); 117 | 118 | if home_state.node_synched { 119 | create_tx_button = 120 | create_tx_button.on_press(Interaction::WalletOperationHomeActionMenuViewInteraction( 121 | LocalViewInteraction::SelectAction(Action::CreateTx), 122 | )) 123 | } 124 | 125 | let create_tx_button: Element = create_tx_button.into(); 126 | 127 | let apply_tx_container = 128 | Container::new(Text::new(localized_string("wallet-apply-tx")).size(DEFAULT_FONT_SIZE)) 129 | .width(button_width) 130 | .align_y(alignment::Vertical::Center) 131 | .align_x(alignment::Horizontal::Center); 132 | 133 | let mut apply_tx_button = Button::new(apply_tx_container) 134 | .width(button_width) 135 | .style(grin_gui_core::theme::ButtonStyle::Primary); 136 | 137 | if home_state.node_synched { 138 | apply_tx_button = 139 | apply_tx_button.on_press(Interaction::WalletOperationHomeActionMenuViewInteraction( 140 | LocalViewInteraction::SelectAction(Action::ApplyTx), 141 | )) 142 | } 143 | 144 | let apply_tx_button: Element = apply_tx_button.into(); 145 | 146 | // TODO refactor since many of the buttons around the UI repeat this theme 147 | let create_container = Container::new(create_tx_button.map(Message::Interaction)).padding(1); 148 | let create_container = Container::new(create_container) 149 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 150 | .padding(1); 151 | 152 | let apply_container = Container::new(apply_tx_button.map(Message::Interaction)).padding(1); 153 | let apply_container = Container::new(apply_container) 154 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 155 | .padding(1); 156 | 157 | let menu_column = Row::new() 158 | .push(description_container) 159 | .push(Space::with_width(Length::Fixed(DEFAULT_PADDING))) 160 | .push(create_container) 161 | .push(Space::with_width(Length::Fixed(DEFAULT_PADDING))) 162 | .push(apply_container); 163 | 164 | Container::new(menu_column).padding(iced::Padding::from([ 165 | 5, // top 166 | 5, // right 167 | 5, // bottom 168 | 5, // left 169 | ])) 170 | } 171 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/chart.rs: -------------------------------------------------------------------------------- 1 | extern crate iced; 2 | extern crate plotters; 3 | 4 | use crate::gui::{element::DEFAULT_PADDING, Message}; 5 | use chrono::{DateTime, Utc}; 6 | use grin_gui_core::theme::{ 7 | Button, Column, Container, Element, PickList, Row, Scrollable, TableRow, Text, TextInput, Theme, 8 | }; 9 | use iced::{ 10 | alignment::{Horizontal, Vertical}, 11 | executor, 12 | mouse::Cursor, 13 | widget::{ 14 | canvas::{self, event, Cache, Frame, Geometry}, 15 | Space, 16 | }, 17 | Alignment, Command, Font, Length, Point, Settings, Size, Subscription, 18 | }; 19 | 20 | use plotters::{ 21 | coord::{types::RangedCoordf32, ReverseCoordTranslate}, 22 | prelude::*, 23 | }; 24 | use plotters_backend::DrawingBackend; 25 | use plotters_iced::{Chart, ChartWidget}; 26 | use std::time::{Duration, Instant}; 27 | use std::{borrow::Borrow, collections::VecDeque}; 28 | 29 | const CHART_CAPTION_HEAD: u16 = 20; 30 | const CHART_CAPTION_SUB: u16 = 12; 31 | 32 | const FONT_REGULAR: Font = Font::with_name("notosans-regular.ttf"); 33 | const FONT_BOLD: Font = Font::with_name("notosans-bold.ttf"); 34 | 35 | #[derive(Default)] 36 | pub struct BalanceChart { 37 | data_points: VecDeque<(DateTime, f64)>, 38 | cursor_index: Option, 39 | caption_index: Option, 40 | theme: Theme, 41 | } 42 | 43 | impl BalanceChart { 44 | /// Create a new chart widget 45 | /// `data` is an iterator of `(DateTime, f64)` tuples in descending order - newest datetime first 46 | pub fn new( 47 | theme: Theme, 48 | data: impl Iterator, f64)>, 49 | cursor_index: Option, 50 | caption_index: Option, 51 | ) -> Element<'static, Message> { 52 | let data_points: VecDeque<_> = data.collect(); 53 | let chart = BalanceChart { 54 | data_points, 55 | theme, 56 | cursor_index, 57 | caption_index, 58 | }; 59 | 60 | Container::new( 61 | Column::new() 62 | .width(Length::Fill) 63 | .height(Length::Fill) 64 | .spacing(5) 65 | .push( 66 | ChartWidget::new(chart).height(Length::Fill), /* .resolve_font( 67 | |_, style| match style { 68 | plotters_backend::FontStyle::Bold => FONT_BOLD, 69 | _ => FONT_REGULAR, 70 | },*/ 71 | ), 72 | ) 73 | .width(Length::Fill) 74 | .height(Length::Fill) 75 | .align_x(Horizontal::Center) 76 | .align_y(Vertical::Center) 77 | .into() 78 | } 79 | 80 | pub fn push_data(&mut self, time: DateTime, value: f64) { 81 | self.data_points.push_front((time, value)); 82 | } 83 | } 84 | 85 | impl Chart for BalanceChart { 86 | type State = (); 87 | 88 | fn update( 89 | &self, 90 | _state: &mut Self::State, 91 | event: canvas::Event, 92 | bounds: iced::Rectangle, 93 | cursor: Cursor, 94 | ) -> (iced_core::event::Status, Option) { 95 | if let Cursor::Available(point) = cursor { 96 | match event { 97 | canvas::Event::Mouse(_evt) if bounds.contains(point) => { 98 | let p_origin = bounds.position(); 99 | let p = point - p_origin; 100 | let percent = p.x / bounds.width; 101 | 102 | let len = self.data_points.len() - 1; 103 | 104 | let approx_index = len as f32 * percent; 105 | let cursor_index = len.saturating_sub(approx_index.floor() as usize); 106 | let mut caption_index = cursor_index; 107 | 108 | // TODO the caption width 55 here should be dynamic based on the width of the caption text 109 | // USD value is 55px wide 110 | // BTC value is ??px wide 111 | // Grin value is ??px wide 112 | let caption_width = 55.0; 113 | 114 | // caption index is cursor index until the caption reaches the edge of the chart 115 | if p.x / (bounds.width - caption_width) >= 1.0 { 116 | let tail = caption_width / bounds.width; 117 | let approx_tail = len as f32 * tail; 118 | caption_index = approx_tail.floor() as usize; 119 | } 120 | 121 | return ( 122 | iced_core::event::Status::Captured, 123 | Some(Message::Interaction( 124 | crate::gui::Interaction::WalletOperationHomeViewInteraction( 125 | super::home::LocalViewInteraction::MouseIndex( 126 | cursor_index, 127 | caption_index, 128 | ), 129 | ), 130 | )), 131 | ); 132 | } 133 | _ => { 134 | return ( 135 | iced_core::event::Status::Captured, 136 | Some(Message::Interaction( 137 | crate::gui::Interaction::WalletOperationHomeViewInteraction( 138 | super::home::LocalViewInteraction::MouseExit, 139 | ), 140 | )), 141 | ); 142 | } 143 | } 144 | } 145 | (event::Status::Ignored, None) 146 | } 147 | 148 | fn build_chart(&self, _state: &Self::State, mut chart: ChartBuilder) { 149 | use plotters::{prelude::*, style::Color}; 150 | 151 | const PLOT_LINE_COLOR: RGBColor = RGBColor(0, 175, 255); 152 | 153 | // Acquire time range 154 | let newest_time = self 155 | .data_points 156 | .front() 157 | .unwrap_or(&(chrono::Utc::now(), 0.0)) 158 | .0; 159 | 160 | let mut oldest_time = self 161 | .data_points 162 | .back() 163 | .unwrap_or(&(chrono::Utc::now() - chrono::Duration::days(7), 0.0)) 164 | .0; 165 | 166 | if newest_time == oldest_time { 167 | oldest_time = chrono::Utc::now() - chrono::Duration::days(7); 168 | } 169 | 170 | // get largest amount from data points 171 | let mut max_value = 0.0; 172 | for (_, amount) in self.data_points.iter() { 173 | if *amount > max_value { 174 | max_value = *amount; 175 | } 176 | } 177 | // we add 10% to the max value to make sure the chart is not cut off 178 | max_value = max_value * 1.1; 179 | 180 | let mut chart = chart 181 | .x_label_area_size(6) 182 | .y_label_area_size(0) 183 | .build_cartesian_2d(oldest_time..newest_time, 0.0_f64..max_value) 184 | .expect("failed to build chart"); 185 | 186 | let chart_color = self.theme.palette.bright.primary; 187 | let chart_color = RGBColor( 188 | (chart_color.r * 255.0) as u8, 189 | (chart_color.g * 255.0) as u8, 190 | (chart_color.b * 255.0) as u8, 191 | ); 192 | 193 | let date_color = self.theme.palette.normal.surface; 194 | let date_color = RGBColor( 195 | (date_color.r * 255.0) as u8, 196 | (date_color.g * 255.0) as u8, 197 | (date_color.b * 255.0) as u8, 198 | ); 199 | 200 | let background_color = self.theme.palette.base.background; 201 | let background_color = RGBColor( 202 | (background_color.r * 255.0) as u8, 203 | (background_color.g * 255.0) as u8, 204 | (background_color.b * 255.0) as u8, 205 | ); 206 | 207 | let text_color = self.theme.palette.bright.surface; 208 | let text_color = RGBColor( 209 | (text_color.r * 255.0) as u8, 210 | (text_color.g * 255.0) as u8, 211 | (text_color.b * 255.0) as u8, 212 | ); 213 | 214 | chart 215 | .configure_mesh() 216 | .bold_line_style(background_color) 217 | .light_line_style(background_color) 218 | .axis_style(background_color) 219 | .x_labels(4) 220 | .x_label_style( 221 | ("sans-serif", 15) 222 | .into_font() 223 | .color(&date_color) 224 | .transform(FontTransform::Rotate90), 225 | ) 226 | .x_label_formatter(&|x| format!("{}", x.format("%b %d, %Y"))) 227 | .draw() 228 | .expect("failed to draw chart mesh"); 229 | 230 | chart 231 | .draw_series( 232 | AreaSeries::new( 233 | self.data_points.iter().map(|x| (x.0, x.1 as f64)), 234 | 0.0, 235 | chart_color.mix(0.075), 236 | ) 237 | .border_style(ShapeStyle::from(chart_color).stroke_width(2)), 238 | ) 239 | .expect("failed to draw chart data"); 240 | 241 | if let Some(cursor) = self.cursor_index { 242 | let caption_index = self.caption_index.unwrap(); 243 | let (time1, amount) = self.data_points[cursor]; 244 | let (time2, _) = self.data_points[caption_index]; 245 | //debug!("index: {}, time: {}, amount: {}", index, time, amount); 246 | 247 | // draws a circle at (date, balance) point of the chart 248 | chart 249 | .draw_series(std::iter::once(Circle::new( 250 | (time1, amount), 251 | 5_i32, 252 | chart_color.filled(), 253 | ))) 254 | .expect("Failed to draw hover point"); 255 | 256 | // draw balance above the point 257 | chart 258 | .draw_series(std::iter::once(Text::new( 259 | format!("{}", amount), 260 | (time2, max_value), 261 | ("sans-serif", CHART_CAPTION_HEAD) 262 | .into_font() 263 | .color(&text_color.mix(1.0)), 264 | ))) 265 | .expect("Failed to draw text"); 266 | 267 | // date below balance with a slight faded color 268 | chart 269 | .draw_series(std::iter::once(Text::new( 270 | format!("{}", time1.format("%b %d, %Y")), 271 | (time2, max_value * 0.84), 272 | ("sans-serif", CHART_CAPTION_SUB) 273 | .into_font() 274 | .color(&text_color.mix(0.7)), 275 | ))) 276 | .expect("Failed to draw text"); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod action_menu; 2 | pub mod apply_tx; 3 | pub mod apply_tx_confirm; 4 | pub mod chart; 5 | pub mod create_tx; 6 | pub mod create_tx_contracts; 7 | pub mod home; 8 | pub mod open; 9 | pub mod show_slatepack; 10 | pub mod tx_detail; 11 | pub mod tx_done; 12 | pub mod tx_list; 13 | pub mod tx_list_display; 14 | pub mod tx_proof; 15 | 16 | use { 17 | crate::gui::{GrinGui, Message}, 18 | crate::Result, 19 | grin_gui_core::config::{Config, TxMethod}, 20 | grin_gui_core::theme::ColorPalette, 21 | grin_gui_core::theme::{ 22 | Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 23 | }, 24 | iced::{Command, Length}, 25 | }; 26 | 27 | pub struct StateContainer { 28 | pub mode: Mode, 29 | pub open_state: open::StateContainer, 30 | pub home_state: home::StateContainer, 31 | pub create_tx_state: create_tx::StateContainer, 32 | pub create_tx_contracts_state: create_tx_contracts::StateContainer, 33 | pub show_slatepack_state: show_slatepack::StateContainer, 34 | pub apply_tx_state: apply_tx::StateContainer, 35 | pub tx_detail_state: tx_detail::StateContainer, 36 | pub tx_proof_state: tx_proof::StateContainer, 37 | pub tx_done_state: tx_done::StateContainer, 38 | // When changed to true, this should stay false until a wallet is opened with a password 39 | has_wallet_open_check_failed_one_time: bool, 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 43 | pub enum Mode { 44 | Open, 45 | Home, 46 | CreateTx, 47 | ApplyTx, 48 | ShowSlatepack, 49 | TxDetail, 50 | TxProof, 51 | TxDone, 52 | } 53 | 54 | impl Default for StateContainer { 55 | fn default() -> Self { 56 | Self { 57 | mode: Mode::Home, 58 | open_state: Default::default(), 59 | home_state: Default::default(), 60 | create_tx_state: Default::default(), 61 | create_tx_contracts_state: Default::default(), 62 | show_slatepack_state: Default::default(), 63 | apply_tx_state: Default::default(), 64 | tx_detail_state: Default::default(), 65 | tx_proof_state: Default::default(), 66 | tx_done_state: Default::default(), 67 | has_wallet_open_check_failed_one_time: false, 68 | } 69 | } 70 | } 71 | 72 | impl StateContainer { 73 | pub fn wallet_not_open(&self) -> bool { 74 | self.has_wallet_open_check_failed_one_time 75 | } 76 | 77 | pub fn set_wallet_not_open(&mut self) { 78 | self.has_wallet_open_check_failed_one_time = true; 79 | self.mode = Mode::Open; 80 | } 81 | 82 | pub fn clear_wallet_not_open(&mut self) { 83 | self.has_wallet_open_check_failed_one_time = false; 84 | } 85 | } 86 | 87 | #[derive(Debug, Clone)] 88 | pub enum LocalViewInteraction {} 89 | 90 | pub fn handle_message( 91 | grin_gui: &mut GrinGui, 92 | message: LocalViewInteraction, 93 | ) -> Result> { 94 | Ok(Command::none()) 95 | } 96 | 97 | pub fn data_container<'a>(state: &'a StateContainer, config: &'a Config) -> Container<'a, Message> { 98 | let content = match state.mode { 99 | Mode::Open => open::data_container(&state.open_state, config), 100 | Mode::Home => home::data_container(config, &state.home_state), 101 | Mode::CreateTx => match config.tx_method { 102 | TxMethod::Legacy => create_tx::data_container(config, &state.create_tx_state), 103 | TxMethod::Contracts => { 104 | create_tx_contracts::data_container(config, &state.create_tx_contracts_state) 105 | } 106 | }, 107 | Mode::ShowSlatepack => show_slatepack::data_container(config, &state.show_slatepack_state), 108 | Mode::ApplyTx => apply_tx::data_container(config, &state.apply_tx_state), 109 | Mode::TxDetail => tx_detail::data_container(config, &state.tx_detail_state), 110 | Mode::TxProof => tx_proof::data_container(config, &state.tx_proof_state), 111 | Mode::TxDone => tx_done::data_container(config, &state.tx_done_state), 112 | }; 113 | 114 | let column = Column::new().push(content); 115 | 116 | Container::new(column) 117 | .center_y() 118 | .center_x() 119 | .width(Length::Fill) 120 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 121 | } 122 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/show_slatepack.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::super::super::{ 3 | BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, 4 | SMALLER_FONT_SIZE, 5 | }, 6 | crate::gui::{GrinGui, Interaction, Message}, 7 | crate::localization::localized_string, 8 | crate::Result, 9 | grin_gui_core::config::Config, 10 | grin_gui_core::theme::ColorPalette, 11 | grin_gui_core::theme::{ 12 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 13 | }, 14 | iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, 15 | iced::{alignment, Alignment, Command, Length}, 16 | iced_aw::Card, 17 | }; 18 | 19 | pub struct StateContainer { 20 | // Encrypted slate to send to recipient 21 | pub encrypted_slate: Option, 22 | // Where the 'submit' or back button leads to 23 | pub submit_mode: Option, 24 | // Label to display as title 25 | pub title_label: String, 26 | // description 27 | pub desc: String, 28 | } 29 | 30 | impl Default for StateContainer { 31 | fn default() -> Self { 32 | Self { 33 | encrypted_slate: Default::default(), 34 | submit_mode: None, 35 | title_label: localized_string("tx-view"), 36 | desc: localized_string("tx-view-desc"), 37 | } 38 | } 39 | } 40 | 41 | impl StateContainer { 42 | pub fn reset_defaults(&mut self) { 43 | self.title_label = localized_string("tx-view"); 44 | self.desc = localized_string("tx-view-desc"); 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone)] 49 | pub enum LocalViewInteraction { 50 | Submit, 51 | } 52 | 53 | pub fn handle_message( 54 | grin_gui: &mut GrinGui, 55 | message: LocalViewInteraction, 56 | ) -> Result> { 57 | let state = &mut grin_gui.wallet_state.operation_state.show_slatepack_state; 58 | match message { 59 | LocalViewInteraction::Submit => { 60 | state.encrypted_slate = None; 61 | state.reset_defaults(); 62 | if let Some(ref m) = state.submit_mode { 63 | grin_gui.wallet_state.operation_state.mode = m.clone(); 64 | } else { 65 | grin_gui.wallet_state.operation_state.mode = 66 | crate::gui::element::wallet::operation::Mode::Home; 67 | } 68 | state.submit_mode = None; 69 | } 70 | } 71 | Ok(Command::none()) 72 | } 73 | 74 | pub fn data_container<'a>( 75 | _config: &'a Config, 76 | state: &'a StateContainer, 77 | ) -> Container<'a, Message> { 78 | // Title row 79 | let title = Text::new(state.title_label.clone()) 80 | .size(DEFAULT_HEADER_FONT_SIZE) 81 | .horizontal_alignment(alignment::Horizontal::Center); 82 | 83 | let title_container = Container::new(title) 84 | .style(grin_gui_core::theme::ContainerStyle::BrightBackground) 85 | .padding(iced::Padding::from([ 86 | 2, // top 87 | 0, // right 88 | 2, // bottom 89 | 5, // left 90 | ])); 91 | 92 | // push more items on to header here: e.g. other buttons, things that belong on the header 93 | let header_row = Row::new().push(title_container); 94 | 95 | let header_container = Container::new(header_row).padding(iced::Padding::from([ 96 | 0, // top 97 | 0, // right 98 | DEFAULT_PADDING as u16, // bottom 99 | 0, // left 100 | ])); 101 | 102 | let description = Text::new(state.desc.clone()) 103 | .size(DEFAULT_FONT_SIZE) 104 | .horizontal_alignment(alignment::Horizontal::Center); 105 | let description_container = 106 | Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 107 | 108 | let card_contents = match &state.encrypted_slate { 109 | Some(s) => s.to_owned(), 110 | None => "".to_owned(), 111 | }; 112 | 113 | let encrypted_slate_card = Card::new( 114 | Text::new(localized_string("tx-paste-success-title")).size(DEFAULT_HEADER_FONT_SIZE), 115 | Text::new(card_contents.clone()).size(DEFAULT_FONT_SIZE), 116 | ) 117 | .foot( 118 | Column::new() 119 | .spacing(10) 120 | .padding(5) 121 | .width(Length::Fill) 122 | .align_items(Alignment::Center) 123 | .push( 124 | Button::new( 125 | Text::new(localized_string("copy-to-clipboard")) 126 | .size(SMALLER_FONT_SIZE) 127 | .horizontal_alignment(alignment::Horizontal::Center), 128 | ) 129 | .style(grin_gui_core::theme::ButtonStyle::NormalText) 130 | .on_press(Message::Interaction(Interaction::WriteToClipboard( 131 | card_contents.clone(), 132 | ))), 133 | ), 134 | ) 135 | .max_width(400.0) 136 | .style(grin_gui_core::theme::CardStyle::Normal); 137 | 138 | let unit_spacing = 15.0; 139 | 140 | let button_height = Length::Fixed(BUTTON_HEIGHT); 141 | let button_width = Length::Fixed(BUTTON_WIDTH); 142 | 143 | let cancel_button_label_container = 144 | Container::new(Text::new(localized_string("ok-caps")).size(DEFAULT_FONT_SIZE)) 145 | .width(button_width) 146 | .height(button_height) 147 | .center_x() 148 | .center_y() 149 | .align_x(alignment::Horizontal::Center); 150 | 151 | let cancel_button: Element = Button::new(cancel_button_label_container) 152 | .style(grin_gui_core::theme::ButtonStyle::Primary) 153 | .on_press(Interaction::WalletOperationShowSlatepackViewInteraction( 154 | LocalViewInteraction::Submit, 155 | )) 156 | .into(); 157 | 158 | let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); 159 | let cancel_container = Container::new(cancel_container) 160 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 161 | .padding(1); 162 | 163 | let unit_spacing = 15.0; 164 | let button_row = Row::new().push(cancel_container); 165 | 166 | let column = Column::new() 167 | .push(description_container) 168 | .push(Space::new( 169 | Length::Fixed(0.0), 170 | Length::Fixed(unit_spacing + 5.0), 171 | )) 172 | .push(encrypted_slate_card) 173 | .push(Space::new( 174 | Length::Fixed(0.0), 175 | Length::Fixed(unit_spacing + 10.0), 176 | )) 177 | .push(button_row) 178 | .push(Space::new( 179 | Length::Fixed(0.0), 180 | Length::Fixed(unit_spacing + 10.0), 181 | )); 182 | 183 | let form_container = Container::new(column) 184 | .width(Length::Fill) 185 | .padding(iced::Padding::from([ 186 | 0, // top 187 | 0, // right 188 | 0, // bottom 189 | 5, // left 190 | ])); 191 | 192 | // form container should be scrollable in tiny windows 193 | let scrollable = Scrollable::new(form_container) 194 | .height(Length::Fill) 195 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 196 | 197 | let content = Container::new(scrollable) 198 | .width(Length::Fill) 199 | .height(Length::Shrink) 200 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 201 | 202 | let wrapper_column = Column::new() 203 | .height(Length::Fill) 204 | .push(header_container) 205 | .push(content); 206 | 207 | // Returns the final container. 208 | Container::new(wrapper_column).padding(iced::Padding::from([ 209 | DEFAULT_PADDING, // top 210 | DEFAULT_PADDING, // right 211 | DEFAULT_PADDING, // bottom 212 | DEFAULT_PADDING, // left 213 | ])) 214 | } 215 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/tx_done.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::super::super::{ 3 | BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, 4 | SMALLER_FONT_SIZE, 5 | }, 6 | crate::gui::{GrinGui, Interaction, Message}, 7 | crate::localization::localized_string, 8 | crate::Result, 9 | grin_gui_core::config::Config, 10 | grin_gui_core::theme::ColorPalette, 11 | grin_gui_core::theme::{ 12 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 13 | }, 14 | iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, 15 | iced::{alignment, Alignment, Command, Length}, 16 | iced_aw::Card, 17 | }; 18 | 19 | pub struct StateContainer {} 20 | 21 | impl Default for StateContainer { 22 | fn default() -> Self { 23 | Self {} 24 | } 25 | } 26 | 27 | impl StateContainer {} 28 | 29 | #[derive(Debug, Clone)] 30 | pub enum LocalViewInteraction { 31 | Submit, 32 | } 33 | 34 | pub fn handle_message( 35 | grin_gui: &mut GrinGui, 36 | message: LocalViewInteraction, 37 | ) -> Result> { 38 | let state = &mut grin_gui.wallet_state.operation_state.tx_done_state; 39 | match message { 40 | LocalViewInteraction::Submit => { 41 | grin_gui.wallet_state.operation_state.mode = 42 | crate::gui::element::wallet::operation::Mode::Home; 43 | } 44 | } 45 | Ok(Command::none()) 46 | } 47 | 48 | pub fn data_container<'a>( 49 | _config: &'a Config, 50 | state: &'a StateContainer, 51 | ) -> Container<'a, Message> { 52 | // Title row 53 | let title = Text::new(localized_string("tx-done")) 54 | .size(DEFAULT_HEADER_FONT_SIZE) 55 | .horizontal_alignment(alignment::Horizontal::Center); 56 | 57 | let title_container = Container::new(title) 58 | .style(grin_gui_core::theme::ContainerStyle::BrightBackground) 59 | .padding(iced::Padding::from([ 60 | 2, // top 61 | 0, // right 62 | 2, // bottom 63 | 5, // left 64 | ])); 65 | 66 | // push more items on to header here: e.g. other buttons, things that belong on the header 67 | let header_row = Row::new().push(title_container); 68 | 69 | let header_container = Container::new(header_row).padding(iced::Padding::from([ 70 | 0, // top 71 | 0, // right 72 | DEFAULT_PADDING as u16, // bottom 73 | 0, // left 74 | ])); 75 | 76 | let description = Text::new(localized_string("tx-done-instruction")) 77 | .size(DEFAULT_FONT_SIZE) 78 | .horizontal_alignment(alignment::Horizontal::Center); 79 | let description_container = 80 | Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 81 | 82 | let unit_spacing = 15.0; 83 | 84 | let button_height = Length::Fixed(BUTTON_HEIGHT); 85 | let button_width = Length::Fixed(BUTTON_WIDTH); 86 | 87 | let cancel_button_label_container = 88 | Container::new(Text::new(localized_string("ok-caps")).size(DEFAULT_FONT_SIZE)) 89 | .width(button_width) 90 | .height(button_height) 91 | .center_x() 92 | .center_y() 93 | .align_x(alignment::Horizontal::Center); 94 | 95 | let cancel_button: Element = Button::new(cancel_button_label_container) 96 | .style(grin_gui_core::theme::ButtonStyle::Primary) 97 | .on_press(Interaction::WalletOperationTxDoneViewInteraction( 98 | LocalViewInteraction::Submit, 99 | )) 100 | .into(); 101 | 102 | let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); 103 | let cancel_container = Container::new(cancel_container) 104 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 105 | .padding(1); 106 | 107 | let unit_spacing = 15.0; 108 | let button_row = Row::new().push(cancel_container); 109 | 110 | let column = Column::new() 111 | .push(description_container) 112 | .push(Space::new( 113 | Length::Fixed(0.0), 114 | Length::Fixed(unit_spacing + 5.0), 115 | )) 116 | .push(button_row) 117 | .push(Space::new( 118 | Length::Fixed(0.0), 119 | Length::Fixed(unit_spacing + 10.0), 120 | )); 121 | 122 | let form_container = Container::new(column) 123 | .width(Length::Fill) 124 | .padding(iced::Padding::from([ 125 | 0, // top 126 | 0, // right 127 | 0, // bottom 128 | 5, // left 129 | ])); 130 | 131 | // form container should be scrollable in tiny windows 132 | let scrollable = Scrollable::new(form_container) 133 | .height(Length::Fill) 134 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 135 | 136 | let content = Container::new(scrollable) 137 | .width(Length::Fill) 138 | .height(Length::Shrink) 139 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 140 | 141 | let wrapper_column = Column::new() 142 | .height(Length::Fill) 143 | .push(header_container) 144 | .push(content); 145 | 146 | // Returns the final container. 147 | Container::new(wrapper_column).padding(iced::Padding::from([ 148 | DEFAULT_PADDING, // top 149 | DEFAULT_PADDING, // right 150 | DEFAULT_PADDING, // bottom 151 | DEFAULT_PADDING, // left 152 | ])) 153 | } 154 | -------------------------------------------------------------------------------- /src/gui/element/wallet/operation/tx_proof.rs: -------------------------------------------------------------------------------- 1 | use super::tx_list::{self, ExpandType}; 2 | use crate::log_error; 3 | use async_std::prelude::FutureExt; 4 | use grin_gui_core::{ 5 | config::Config, 6 | error::GrinWalletInterfaceError, 7 | wallet::{InvoiceProof, SlatepackAddress, TxLogEntry, TxLogEntryType}, 8 | }; 9 | use iced_aw::Card; 10 | use iced_core::Widget; 11 | use std::fs::File; 12 | use std::io::{Read, Write}; 13 | use std::path::PathBuf; 14 | 15 | use super::tx_list::{HeaderState, TxList}; 16 | 17 | use { 18 | super::super::super::{ 19 | BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, 20 | SMALLER_FONT_SIZE, 21 | }, 22 | crate::gui::{GrinGui, Interaction, Message}, 23 | crate::localization::localized_string, 24 | crate::Result, 25 | anyhow::Context, 26 | grin_gui_core::theme::{ 27 | Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 28 | }, 29 | grin_gui_core::wallet::{InitTxArgs, Slate, StatusMessage, WalletInfo, WalletInterface}, 30 | grin_gui_core::{ 31 | node::{amount_from_hr_string, amount_to_hr_string}, 32 | theme::{ButtonStyle, ColorPalette, ContainerStyle}, 33 | }, 34 | iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, 35 | iced::{alignment, Alignment, Command, Length}, 36 | serde::{Deserialize, Serialize}, 37 | std::sync::{Arc, RwLock}, 38 | }; 39 | 40 | pub struct StateContainer { 41 | // Transaction that we're viewing 42 | pub current_tx: Option, 43 | pub current_proof: Option, 44 | } 45 | 46 | impl Default for StateContainer { 47 | fn default() -> Self { 48 | Self { 49 | current_tx: Default::default(), 50 | current_proof: Default::default(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 56 | pub enum Action {} 57 | 58 | #[derive(Debug, Clone)] 59 | pub enum LocalViewInteraction { 60 | Back, 61 | } 62 | 63 | pub fn handle_message<'a>( 64 | grin_gui: &mut GrinGui, 65 | message: LocalViewInteraction, 66 | ) -> Result> { 67 | let state = &mut grin_gui.wallet_state.operation_state.create_tx_state; 68 | 69 | match message { 70 | LocalViewInteraction::Back => { 71 | log::debug!("Interaction::WalletOperationTxProofViewInteraction(Back)"); 72 | grin_gui.wallet_state.operation_state.mode = 73 | crate::gui::element::wallet::operation::Mode::Home; 74 | } 75 | } 76 | 77 | Ok(Command::none()) 78 | } 79 | 80 | pub fn data_container<'a>(config: &'a Config, state: &'a StateContainer) -> Container<'a, Message> { 81 | // Title row 82 | let title = Text::new(localized_string("tx-proof-title")) 83 | .size(DEFAULT_HEADER_FONT_SIZE) 84 | .horizontal_alignment(alignment::Horizontal::Center); 85 | 86 | let title_container = Container::new(title) 87 | .style(grin_gui_core::theme::ContainerStyle::BrightBackground) 88 | .padding(iced::Padding::from([ 89 | 2, // top 90 | 0, // right 91 | 2, // bottom 92 | 5, // left 93 | ])); 94 | 95 | let header_row = Row::new().push(title_container); 96 | 97 | let header_container = Container::new(header_row).padding(iced::Padding::from([ 98 | 0, // top 99 | 0, // right 100 | DEFAULT_PADDING as u16, // bottom 101 | 0, // left 102 | ])); 103 | 104 | let unit_spacing = 15.0; 105 | let row_spacing = 5.0; 106 | 107 | let button_height = Length::Fixed(BUTTON_HEIGHT); 108 | let button_width = Length::Fixed(BUTTON_WIDTH); 109 | 110 | let mut column = Column::new(); 111 | 112 | if let Some(ref proof) = state.current_proof { 113 | // Amount 114 | let pr_amount_label = Text::new(format!("{}: ", localized_string("pr-amount"))) 115 | .size(DEFAULT_FONT_SIZE) 116 | .horizontal_alignment(alignment::Horizontal::Left); 117 | 118 | let pr_amount_label_container = Container::new(pr_amount_label) 119 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 120 | 121 | let pr_amount_value = Text::new(format!("{}", amount_to_hr_string(proof.amount, true))) 122 | .size(DEFAULT_FONT_SIZE) 123 | .horizontal_alignment(alignment::Horizontal::Left); 124 | 125 | let pr_amount_value_container = Container::new(pr_amount_value) 126 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 127 | 128 | let pr_amount_row = Row::new() 129 | .push(pr_amount_label_container) 130 | .push(pr_amount_value_container); 131 | column = column 132 | .push(pr_amount_row) 133 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(row_spacing))); 134 | 135 | // Timestamp 136 | let pr_timestamp_label = Text::new(format!("{}: ", localized_string("pr-timestamp"))) 137 | .size(DEFAULT_FONT_SIZE) 138 | .horizontal_alignment(alignment::Horizontal::Left); 139 | 140 | let pr_timestamp_label_container = Container::new(pr_timestamp_label) 141 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 142 | 143 | // convert i64 timestamp to utc time 144 | let ts_display = chrono::NaiveDateTime::from_timestamp(proof.timestamp, 0) 145 | .format("%Y-%m-%d %H:%M:%S") 146 | .to_string(); 147 | 148 | let pr_timestamp_value = Text::new(format!("{} UTC", ts_display)) 149 | .size(DEFAULT_FONT_SIZE) 150 | .horizontal_alignment(alignment::Horizontal::Left); 151 | 152 | let pr_timestamp_value_container = Container::new(pr_timestamp_value) 153 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 154 | 155 | let pr_timestamp_row = Row::new() 156 | .push(pr_timestamp_label_container) 157 | .push(pr_timestamp_value_container); 158 | column = column 159 | .push(pr_timestamp_row) 160 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(row_spacing))); 161 | 162 | // sender address 163 | let pr_sender_address_label = 164 | Text::new(format!("{}: ", localized_string("pr-sender-address"))) 165 | .size(DEFAULT_FONT_SIZE) 166 | .horizontal_alignment(alignment::Horizontal::Left); 167 | 168 | let pr_sender_address_label_container = Container::new(pr_sender_address_label) 169 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 170 | 171 | let pr_sender_address_value = 172 | Text::new(format!("{}", SlatepackAddress::new(&proof.sender_address))) 173 | .size(DEFAULT_FONT_SIZE) 174 | .horizontal_alignment(alignment::Horizontal::Left); 175 | 176 | let pr_sender_address_value_container = Container::new(pr_sender_address_value) 177 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 178 | 179 | let pr_sender_address_row = Row::new() 180 | .push(pr_sender_address_label_container) 181 | .push(pr_sender_address_value_container); 182 | column = column 183 | .push(pr_sender_address_row) 184 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(row_spacing))) 185 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(row_spacing))); 186 | 187 | let card_contents = format!("{}", serde_json::to_string_pretty(&proof).unwrap()); 188 | 189 | let json_proof_card = Card::new( 190 | Text::new(localized_string("pr-json-proof")).size(DEFAULT_HEADER_FONT_SIZE), 191 | Text::new(card_contents.clone()).size(DEFAULT_FONT_SIZE), 192 | ) 193 | .foot( 194 | Column::new() 195 | .spacing(10) 196 | .padding(5) 197 | .width(Length::Fill) 198 | .align_items(Alignment::Center) 199 | .push( 200 | Button::new( 201 | Text::new(localized_string("copy-to-clipboard")) 202 | .size(SMALLER_FONT_SIZE) 203 | .horizontal_alignment(alignment::Horizontal::Center), 204 | ) 205 | .style(grin_gui_core::theme::ButtonStyle::NormalText) 206 | .on_press(Message::Interaction(Interaction::WriteToClipboard( 207 | card_contents.clone(), 208 | ))), 209 | ), 210 | ) 211 | .max_width(400.0) 212 | .style(grin_gui_core::theme::CardStyle::Normal); 213 | 214 | let json_proof_row = Row::new().push(json_proof_card); 215 | 216 | column = column 217 | .push(json_proof_row) 218 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(row_spacing))); 219 | } 220 | 221 | let cancel_button_label_container = 222 | Container::new(Text::new(localized_string("back")).size(DEFAULT_FONT_SIZE)) 223 | .width(button_width) 224 | .height(button_height) 225 | .center_x() 226 | .center_y() 227 | .align_x(alignment::Horizontal::Center); 228 | 229 | let cancel_button: Element = Button::new(cancel_button_label_container) 230 | .style(grin_gui_core::theme::ButtonStyle::Primary) 231 | .on_press(Interaction::WalletOperationTxProofViewInteraction( 232 | LocalViewInteraction::Back, 233 | )) 234 | .into(); 235 | 236 | let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); 237 | let cancel_container = Container::new(cancel_container) 238 | .style(grin_gui_core::theme::ContainerStyle::Segmented) 239 | .padding(1); 240 | 241 | let button_row = Row::new() 242 | .push(cancel_container) 243 | .push(Space::new(Length::Fixed(unit_spacing), Length::Fixed(0.0))); 244 | 245 | column = column.push(button_row); 246 | 247 | let form_container = Container::new(column) 248 | .width(Length::Fill) 249 | .padding(iced::Padding::from([ 250 | 0, // top 251 | 0, // right 252 | 0, // bottom 253 | 5, // left 254 | ])); 255 | 256 | // form container should be scrollable in tiny windows 257 | let scrollable = Scrollable::new(form_container) 258 | .height(Length::Fill) 259 | .style(grin_gui_core::theme::ScrollableStyle::Primary); 260 | 261 | let content = Container::new(scrollable) 262 | .width(Length::Fill) 263 | .height(Length::Shrink) 264 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground); 265 | 266 | let wrapper_column = Column::new() 267 | .height(Length::Fill) 268 | .push(header_container) 269 | .push(content); 270 | 271 | // Returns the final container. 272 | Container::new(wrapper_column).padding(iced::Padding::from([ 273 | DEFAULT_PADDING, // top 274 | DEFAULT_PADDING, // right 275 | DEFAULT_PADDING, // bottom 276 | DEFAULT_PADDING, // left 277 | ])) 278 | } 279 | -------------------------------------------------------------------------------- /src/gui/element/wallet/setup/init.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE}, 3 | crate::gui::{element::settings::wallet, GrinGui, Interaction, Message}, 4 | crate::localization::localized_string, 5 | crate::Result, 6 | grin_gui_core::theme::{ 7 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 8 | }, 9 | grin_gui_core::{ 10 | theme::ColorPalette, 11 | wallet::{create_grin_wallet_path, ChainTypes}, 12 | }, 13 | iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, 14 | iced::{alignment, Alignment, Command, Length}, 15 | }; 16 | 17 | pub struct StateContainer { 18 | pub setup_wallet_defaults_is_selected: bool, 19 | } 20 | 21 | impl Default for StateContainer { 22 | fn default() -> Self { 23 | Self { 24 | setup_wallet_defaults_is_selected: true, 25 | } 26 | } 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub enum LocalViewInteraction { 31 | WalletSetup, 32 | WalletList, 33 | } 34 | 35 | pub fn handle_message( 36 | grin_gui: &mut GrinGui, 37 | message: LocalViewInteraction, 38 | ) -> Result> { 39 | let state = &mut grin_gui.wallet_state.setup_state; 40 | match message { 41 | LocalViewInteraction::WalletSetup => { 42 | let config = &grin_gui.config; 43 | let wallet_default_name = localized_string("wallet-default-name"); 44 | let mut wallet_display_name = wallet_default_name.clone(); 45 | let mut i = 1; 46 | 47 | // wallet display name must be unique: i.e. Default 1, Default 2, ... 48 | while let Some(_) = config 49 | .wallets 50 | .iter() 51 | .find(|wallet| wallet.display_name == wallet_display_name) 52 | { 53 | wallet_display_name = format!("{} {}", wallet_default_name, i); 54 | i += 1; 55 | } 56 | 57 | // i.e. default_1, default_2, ... 58 | let wallet_dir: String = str::replace(&wallet_display_name.to_lowercase(), " ", "_"); 59 | 60 | state 61 | .setup_wallet_state 62 | .advanced_options_state 63 | .top_level_directory = create_grin_wallet_path(&ChainTypes::Mainnet, &wallet_dir); 64 | 65 | state.mode = super::Mode::CreateWallet(wallet_display_name); 66 | } 67 | /*LocalViewInteraction::WalletSetupRestore => { 68 | //state.mode = super::Mode::RestoreWallet; 69 | },*/ 70 | LocalViewInteraction::WalletList => state.mode = super::Mode::ListWallets, 71 | } 72 | Ok(Command::none()) 73 | } 74 | 75 | pub fn data_container<'a>() -> Container<'a, Message> { 76 | // Title row 77 | let title = Text::new(localized_string("setup-grin-first-time")) 78 | .size(DEFAULT_HEADER_FONT_SIZE) 79 | .horizontal_alignment(alignment::Horizontal::Center); 80 | 81 | let title_container = 82 | Container::new(title).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 83 | 84 | let title_row = Row::new() 85 | .push(title_container) 86 | .align_items(Alignment::Center) 87 | .padding(6) 88 | .spacing(20); 89 | 90 | let description = Text::new(localized_string("setup-grin-wallet-description")) 91 | .size(DEFAULT_FONT_SIZE) 92 | .horizontal_alignment(alignment::Horizontal::Left); 93 | let description_container = 94 | Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 95 | 96 | let or_text = Text::new(localized_string("or-caps")) 97 | .size(DEFAULT_FONT_SIZE) 98 | .horizontal_alignment(alignment::Horizontal::Center); 99 | 100 | let or_text_container = 101 | Container::new(or_text).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 102 | 103 | let create_default_wallet_button_label_container = Container::new( 104 | Text::new(localized_string("setup-grin-autogenerate-wallet")).size(DEFAULT_FONT_SIZE), 105 | ) 106 | .center_x() 107 | .align_x(alignment::Horizontal::Center); 108 | 109 | let create_default_wallet_button: Element = 110 | Button::new(create_default_wallet_button_label_container) 111 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 112 | .on_press(Interaction::WalletSetupInitViewInteraction( 113 | LocalViewInteraction::WalletSetup, 114 | )) 115 | .into(); 116 | 117 | let select_wallet_button_label_container = 118 | Container::new(Text::new(localized_string("select-wallet-toml")).size(DEFAULT_FONT_SIZE)) 119 | .center_x() 120 | .align_x(alignment::Horizontal::Center); 121 | 122 | let select_wallet_button: Element = 123 | Button::new(select_wallet_button_label_container) 124 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 125 | .on_press(Interaction::WalletSetupInitViewInteraction( 126 | LocalViewInteraction::WalletList, 127 | )) 128 | .into(); 129 | 130 | let select_wallet_button_container = 131 | Container::new(select_wallet_button.map(Message::Interaction)).center_x(); 132 | 133 | //let mut wallet_setup_modal_column = 134 | /*let wallet_setup_select_column = { 135 | let checkbox = Checkbox::new( 136 | state.setup_wallet_defaults_is_selected, 137 | localized_string("setup-grin-autogenerate-wallet"), 138 | Interaction::ToggleCloseToTray, 139 | ) 140 | .style(grin_gui_core::theme::CheckboxStyles::Normal) 141 | .text_size(DEFAULT_FONT_SIZE) 142 | .spacing(5); 143 | 144 | let checkbox: Element = checkbox.into(); 145 | 146 | let checkbox_container = Container::new(checkbox.map(Message::Interaction)) 147 | .style(grin_gui_core::theme::container::Container::NormalBackground); 148 | 149 | Column::new().push(checkbox_container) 150 | };*/ 151 | 152 | let unit_spacing = 15.0; 153 | 154 | let select_column = Column::new() 155 | .push(create_default_wallet_button.map(Message::Interaction)) 156 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) 157 | .push(or_text_container) 158 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) 159 | .push(select_wallet_button_container) 160 | .align_items(Alignment::Center); 161 | 162 | let column = Column::new() 163 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(20.0))) 164 | .push(title_row) 165 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) 166 | .push(description_container) 167 | .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) 168 | .push(select_column) 169 | .align_items(Alignment::Center); 170 | 171 | Container::new(column) 172 | .center_y() 173 | .center_x() 174 | .width(Length::Fill) 175 | } 176 | -------------------------------------------------------------------------------- /src/gui/element/wallet/setup/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod init; 2 | pub mod wallet_import; 3 | pub mod wallet_import_success; 4 | pub mod wallet_list; 5 | pub mod wallet_setup; 6 | pub mod wallet_success; 7 | 8 | use { 9 | crate::gui::{GrinGui, Message}, 10 | crate::Result, 11 | grin_gui_core::config::Config, 12 | grin_gui_core::theme::ColorPalette, 13 | grin_gui_core::theme::{ 14 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 15 | }, 16 | iced::widget::Space, 17 | iced::{Command, Length}, 18 | }; 19 | 20 | pub struct StateContainer { 21 | pub mode: Mode, 22 | pub setup_init_state: init::StateContainer, 23 | pub setup_wallet_state: wallet_setup::StateContainer, 24 | pub import_wallet_state: wallet_import::StateContainer, 25 | pub setup_wallet_success_state: wallet_success::StateContainer, 26 | pub import_wallet_success_state: wallet_import_success::StateContainer, 27 | pub setup_wallet_list_state: wallet_list::StateContainer, 28 | } 29 | 30 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 31 | pub enum Mode { 32 | Init, 33 | CreateWallet(String), 34 | ListWallets, 35 | WalletCreateSuccess, 36 | WalletImportSuccess, 37 | } 38 | 39 | impl Default for StateContainer { 40 | fn default() -> Self { 41 | Self { 42 | mode: Mode::Init, 43 | setup_init_state: Default::default(), 44 | setup_wallet_state: Default::default(), 45 | import_wallet_state: Default::default(), 46 | setup_wallet_success_state: Default::default(), 47 | import_wallet_success_state: Default::default(), 48 | setup_wallet_list_state: Default::default(), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | pub enum LocalViewInteraction {} 55 | 56 | pub fn handle_message( 57 | grin_gui: &mut GrinGui, 58 | message: LocalViewInteraction, 59 | ) -> Result> { 60 | Ok(Command::none()) 61 | } 62 | 63 | pub fn data_container<'a>(state: &'a StateContainer, config: &Config) -> Container<'a, Message> { 64 | let content = match &state.mode { 65 | Mode::Init => init::data_container(), 66 | Mode::CreateWallet(default_display_name) => { 67 | wallet_setup::data_container(&state.setup_wallet_state, default_display_name) 68 | } 69 | Mode::WalletCreateSuccess => { 70 | wallet_success::data_container(&state.setup_wallet_success_state) 71 | } 72 | Mode::WalletImportSuccess => { 73 | wallet_import_success::data_container(&state.import_wallet_success_state) 74 | } 75 | Mode::ListWallets => wallet_list::data_container(&state.setup_wallet_list_state, config), 76 | }; 77 | 78 | Container::new(content) 79 | .center_y() 80 | .center_x() 81 | .width(Length::Fill) 82 | .style(grin_gui_core::theme::ContainerStyle::NormalBackground) 83 | } 84 | -------------------------------------------------------------------------------- /src/gui/element/wallet/setup/wallet_import_success.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, SMALLER_FONT_SIZE}, 3 | crate::gui::{GrinGui, Interaction, Message}, 4 | crate::localization::localized_string, 5 | crate::Result, 6 | grin_gui_core::theme::ColorPalette, 7 | grin_gui_core::theme::{ 8 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 9 | }, 10 | iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, 11 | iced::{alignment, Alignment, Command, Length}, 12 | iced_aw::Card, 13 | }; 14 | 15 | pub struct StateContainer {} 16 | 17 | impl Default for StateContainer { 18 | fn default() -> Self { 19 | Self {} 20 | } 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub enum LocalViewInteraction { 25 | Submit, 26 | } 27 | 28 | pub fn handle_message( 29 | grin_gui: &mut GrinGui, 30 | message: LocalViewInteraction, 31 | ) -> Result> { 32 | let state = &mut grin_gui.wallet_state.setup_state.import_wallet_state; 33 | match message { 34 | LocalViewInteraction::Submit => { 35 | debug!("Wallet import success view submit"); 36 | grin_gui.wallet_state.mode = super::super::Mode::Operation; 37 | grin_gui.wallet_state.setup_state.mode = crate::gui::element::wallet::setup::Mode::Init; 38 | } 39 | } 40 | Ok(Command::none()) 41 | } 42 | 43 | pub fn data_container<'a>(state: &'a StateContainer) -> Container<'a, Message> { 44 | // Title row 45 | let title = Text::new(localized_string("import-grin-wallet-success")) 46 | .size(DEFAULT_HEADER_FONT_SIZE) 47 | .horizontal_alignment(alignment::Horizontal::Left); 48 | 49 | let title_container = 50 | Container::new(title).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 51 | 52 | let title_row = Row::new() 53 | .push(title_container) 54 | .align_items(Alignment::Center) 55 | .spacing(20); 56 | 57 | let submit_button_label_container = Container::new( 58 | Text::new(localized_string("setup-grin-wallet-done")).size(DEFAULT_FONT_SIZE), 59 | ) 60 | .center_x() 61 | .align_x(alignment::Horizontal::Center); 62 | 63 | let next_button = Button::new(submit_button_label_container) 64 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 65 | .on_press(Interaction::WalletImportWalletSuccessViewInteraction( 66 | LocalViewInteraction::Submit, 67 | )); 68 | 69 | let next_button: Element = next_button.into(); 70 | 71 | let unit_spacing = 15.0; 72 | 73 | let colum = Column::new() 74 | .push(Space::new( 75 | Length::Fixed(0.0), 76 | Length::Fixed(unit_spacing + 5.0), 77 | )) 78 | .push(title_row) 79 | .push(Space::new( 80 | Length::Fixed(0.0), 81 | Length::Fixed(unit_spacing + 5.0), 82 | )) 83 | .push(next_button.map(Message::Interaction)) 84 | .align_items(Alignment::Center); 85 | 86 | Container::new(colum) 87 | .center_y() 88 | .center_x() 89 | .width(Length::Fill) 90 | } 91 | -------------------------------------------------------------------------------- /src/gui/element/wallet/setup/wallet_success.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, SMALLER_FONT_SIZE}, 3 | crate::gui::{GrinGui, Interaction, Message}, 4 | crate::localization::localized_string, 5 | crate::Result, 6 | grin_gui_core::theme::ColorPalette, 7 | grin_gui_core::theme::{ 8 | Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, 9 | }, 10 | iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, 11 | iced::{alignment, Alignment, Command, Length}, 12 | iced_aw::Card, 13 | }; 14 | 15 | pub struct StateContainer { 16 | // TODO: ZeroingString this 17 | pub recovery_phrase: String, 18 | } 19 | 20 | impl Default for StateContainer { 21 | fn default() -> Self { 22 | Self { 23 | recovery_phrase: Default::default(), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub enum LocalViewInteraction { 30 | Submit, 31 | } 32 | 33 | pub fn handle_message( 34 | grin_gui: &mut GrinGui, 35 | message: LocalViewInteraction, 36 | ) -> Result> { 37 | let state = &mut grin_gui.wallet_state.setup_state.setup_wallet_state; 38 | match message { 39 | LocalViewInteraction::Submit => { 40 | grin_gui.wallet_state.mode = super::super::Mode::Operation; 41 | grin_gui.wallet_state.setup_state.mode = crate::gui::element::wallet::setup::Mode::Init; 42 | } 43 | } 44 | Ok(Command::none()) 45 | } 46 | 47 | pub fn data_container<'a>(state: &'a StateContainer) -> Container<'a, Message> { 48 | // Title row 49 | let title = Text::new(localized_string("setup-grin-wallet-success")) 50 | .size(DEFAULT_HEADER_FONT_SIZE) 51 | .horizontal_alignment(alignment::Horizontal::Left); 52 | 53 | let title_container = 54 | Container::new(title).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 55 | 56 | let title_row = Row::new() 57 | .push(title_container) 58 | .align_items(Alignment::Center) 59 | .spacing(20); 60 | 61 | let description = Text::new(localized_string("setup-grin-wallet-recovery-phrase")) 62 | .size(DEFAULT_FONT_SIZE) 63 | .horizontal_alignment(alignment::Horizontal::Center); 64 | let description_container = 65 | Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); 66 | 67 | let recovery_phrase_card = Card::new( 68 | Text::new(localized_string("setup-grin-wallet-recovery-phrase-title")) 69 | .size(DEFAULT_HEADER_FONT_SIZE), 70 | Text::new(&state.recovery_phrase).size(DEFAULT_FONT_SIZE), 71 | ) 72 | .foot( 73 | Column::new() 74 | .spacing(10) 75 | .padding(5) 76 | .width(Length::Fill) 77 | .align_items(Alignment::Center) 78 | .push( 79 | Button::new( 80 | Text::new(localized_string("copy-to-clipboard")) 81 | .size(SMALLER_FONT_SIZE) 82 | .horizontal_alignment(alignment::Horizontal::Center), 83 | ) 84 | .style(grin_gui_core::theme::ButtonStyle::NormalText) 85 | .on_press(Message::Interaction(Interaction::WriteToClipboard( 86 | state.recovery_phrase.clone(), 87 | ))), 88 | ), 89 | ) 90 | .max_width(400.0) 91 | .style(grin_gui_core::theme::CardStyle::Normal); 92 | 93 | let submit_button_label_container = Container::new( 94 | Text::new(localized_string("setup-grin-wallet-done")).size(DEFAULT_FONT_SIZE), 95 | ) 96 | .center_x() 97 | .align_x(alignment::Horizontal::Center); 98 | 99 | let next_button = Button::new(submit_button_label_container) 100 | .style(grin_gui_core::theme::ButtonStyle::Bordered) 101 | .on_press(Interaction::WalletSetupWalletSuccessViewInteraction( 102 | LocalViewInteraction::Submit, 103 | )); 104 | 105 | let next_button: Element = next_button.into(); 106 | 107 | let unit_spacing = 15.0; 108 | 109 | let colum = Column::new() 110 | .push(title_row) 111 | .push(Space::new( 112 | Length::Fixed(0.0), 113 | Length::Fixed(unit_spacing + 5.0), 114 | )) 115 | .push(description_container) 116 | .push(Space::new( 117 | Length::Fixed(0.0), 118 | Length::Fixed(unit_spacing + 5.0), 119 | )) 120 | .push(recovery_phrase_card) 121 | .push(Space::new( 122 | Length::Fixed(0.0), 123 | Length::Fixed(unit_spacing + 10.0), 124 | )) 125 | .push(next_button.map(Message::Interaction)) 126 | .align_items(Alignment::Center); 127 | 128 | Container::new(colum) 129 | .center_y() 130 | .center_x() 131 | .width(Length::Fill) 132 | } 133 | -------------------------------------------------------------------------------- /src/gui/time.rs: -------------------------------------------------------------------------------- 1 | use iced_futures::{self, subscription}; 2 | 3 | use iced_core::Hasher; 4 | use std::hash::Hash; 5 | 6 | pub fn every(duration: std::time::Duration) -> iced::Subscription> { 7 | iced::Subscription::from_recipe(Every(duration)) 8 | } 9 | 10 | struct Every(std::time::Duration); 11 | 12 | impl iced_futures::subscription::Recipe for Every { 13 | type Output = chrono::DateTime; 14 | 15 | fn hash(&self, state: &mut Hasher) { 16 | use std::hash::Hash; 17 | 18 | std::any::TypeId::of::().hash(state); 19 | self.0.hash(state); 20 | } 21 | 22 | fn stream( 23 | self: Box, 24 | _input: subscription::EventStream, 25 | ) -> futures::stream::BoxStream<'static, Self::Output> { 26 | use futures::stream::StreamExt; 27 | 28 | async_std::stream::interval(self.0) 29 | .map(|_| chrono::Local::now()) 30 | .boxed() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/localization.rs: -------------------------------------------------------------------------------- 1 | use json_gettext::{get_text, static_json_gettext_build, JSONGetText}; 2 | use once_cell::sync::{Lazy, OnceCell}; 3 | 4 | use std::sync::RwLock; 5 | 6 | pub static LOCALIZATION_CTX: Lazy> = Lazy::new(|| { 7 | static_json_gettext_build!( 8 | "en_US", 9 | "en_US", 10 | "locale/en.json", 11 | "de_DE", 12 | "locale/de.json" 13 | ) 14 | .unwrap() 15 | }); 16 | 17 | pub static LANG: OnceCell> = OnceCell::new(); 18 | 19 | pub fn localized_string(key: &str) -> String { 20 | let lang = LANG.get().expect("LANG not set").read().unwrap(); 21 | 22 | if let Some(text) = get_text!(LOCALIZATION_CTX, *lang, key) { 23 | let text = text.to_string(); 24 | if text.is_empty() { 25 | key.to_owned() 26 | } else { 27 | text 28 | } 29 | } else { 30 | key.to_owned() 31 | } 32 | } 33 | 34 | /// Returns a localized `timeago::Formatter`. 35 | /// If user has chosen a language whic his not supported by `timeago` we fallback to english. 36 | pub fn localized_timeago_formatter() -> timeago::Formatter> { 37 | let lang = LANG.get().expect("LANG not set").read().unwrap(); 38 | let isolang = isolang::Language::from_locale(&lang).unwrap(); 39 | 40 | // this step might fail if timeago does not support the chosen language. 41 | // In that case we fallback to `en_US`. 42 | if let Some(timeago_lang) = timeago::from_isolang(isolang) { 43 | timeago::Formatter::with_language(timeago_lang) 44 | } else { 45 | timeago::Formatter::with_language(Box::new(timeago::English)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Avoid spawning an console window for the program. 2 | // This is ignored on other platforms. 3 | // https://msdn.microsoft.com/en-us/library/4cc7ya5b.aspx for more information. 4 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 5 | #![allow(dead_code)] 6 | #![allow(unused_variables)] 7 | #![allow(unused_mut)] 8 | #![allow(unused_imports)] 9 | 10 | mod cli; 11 | mod gui; 12 | mod localization; 13 | #[cfg(target_os = "windows")] 14 | mod process; 15 | #[cfg(target_os = "windows")] 16 | mod tray; 17 | 18 | #[macro_use] 19 | extern crate log; 20 | 21 | use grin_gui_core::config::Config; 22 | use grin_gui_core::fs::{PersistentData, CONFIG_DIR}; 23 | use grin_gui_core::utility::{remove_file, rename}; 24 | use grin_gui_core::{logger, LoggingConfig}; 25 | 26 | #[cfg(target_os = "linux")] 27 | use anyhow::Context; 28 | use std::env; 29 | use std::path::Path; 30 | #[cfg(target_os = "linux")] 31 | use std::path::PathBuf; 32 | 33 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 34 | 35 | pub type Result = std::result::Result; 36 | 37 | pub fn main() { 38 | let opts_result = cli::get_opts(); 39 | 40 | #[cfg(debug_assertions)] 41 | let is_debug = true; 42 | #[cfg(not(debug_assertions))] 43 | let is_debug = false; 44 | 45 | // If this is a clap error, we map to None since we are going to exit and display 46 | // an error message anyway and this value won't matter. If it's not an error, 47 | // the underlying `command` will drive this variable. If a `command` is passed 48 | // on the command line, Grin GUI functions as a CLI instead of launching the GUI. 49 | /*let is_cli = opts_result 50 | .as_ref() 51 | .map(|o| &o.command) 52 | .unwrap_or(&None) 53 | .is_some();*/ 54 | 55 | // disable command line for now 56 | let is_cli = false; 57 | 58 | // This function validates whether or not we need to exit and print any message 59 | // due to arguments passed on the command line. If not, it will return a 60 | // parsed `Opts` struct. This also handles setting up our windows release build 61 | // fix that allows us to print to the console when not using the GUI. 62 | let opts = cli::validate_opts_or_exit(opts_result, is_cli, is_debug); 63 | 64 | let config_dir_local = { 65 | let mut config_dir = CONFIG_DIR.lock().unwrap(); 66 | if let Some(data_dir) = &opts.data_directory { 67 | *config_dir = data_dir.clone(); 68 | } 69 | config_dir.clone() 70 | }; 71 | 72 | // Set up logging config for grin-gui code itself 73 | let mut gui_logging_config = LoggingConfig::default(); 74 | gui_logging_config.tui_running = Some(false); 75 | gui_logging_config.stdout_log_level = log::Level::Debug; 76 | gui_logging_config.file_log_level = log::Level::Debug; 77 | let mut gui_log_dir = config_dir_local.clone(); 78 | gui_log_dir.push("grin-gui.log"); 79 | gui_logging_config.log_file_path = gui_log_dir.into_os_string().into_string().unwrap(); 80 | 81 | logger::update_logging_config(logger::LogArea::Gui, gui_logging_config); 82 | 83 | // Called when we launch from the temp (new release) binary during the self update 84 | // process. We will rename the temp file (running process) to the original binary 85 | if let Some(cleanup_path) = &opts.self_update_temp { 86 | if let Err(e) = handle_self_update_temp(cleanup_path) { 87 | log_error(&e); 88 | std::process::exit(1); 89 | } 90 | } 91 | 92 | log_panics::init(); 93 | 94 | log::info!("Grin GUI {} has started.", VERSION); 95 | 96 | // Ensures another instance of Grin GUI isn't already running. 97 | #[cfg(target_os = "windows")] 98 | process::avoid_multiple_instances(); 99 | 100 | /*match opts.command { 101 | Some(command) => { 102 | // Process the command and exit 103 | if let Err(e) = match command { 104 | cli::Command::Backup { 105 | backup_folder, 106 | destination, 107 | flavors, 108 | compression_format, 109 | level, 110 | } => command::backup( 111 | backup_folder, 112 | destination, 113 | flavors, 114 | compression_format, 115 | level, 116 | ), 117 | cli::Command::Update => command::update_both(), 118 | cli::Command::UpdateAddons => command::update_all_addons(), 119 | cli::Command::UpdateAuras => command::update_all_auras(), 120 | cli::Command::Install { url, flavor } => command::install_from_source(url, flavor), 121 | cli::Command::PathAdd { path, flavor } => command::path_add(path, flavor), 122 | } { 123 | log_error(&e); 124 | } 125 | } 126 | None => {*/ 127 | let config: Config = Config::load_or_default().expect("loading config on application startup"); 128 | 129 | #[cfg(target_os = "windows")] 130 | tray::spawn_sys_tray(config.close_to_tray, config.start_closed_to_tray); 131 | 132 | // Start the GUI 133 | gui::run(opts, config); 134 | /* 135 | }*/ 136 | } 137 | 138 | /// Log any errors 139 | pub fn log_error(error: &anyhow::Error) { 140 | log::error!("{}", error); 141 | 142 | let mut causes = error.chain(); 143 | // Remove first entry since it's same as top level error 144 | causes.next(); 145 | 146 | for cause in causes { 147 | log::error!("caused by: {}", cause); 148 | } 149 | } 150 | 151 | pub fn error_cause_string(error: &anyhow::Error) -> String { 152 | let mut ret_val = String::new(); 153 | let mut causes = error.chain(); 154 | 155 | // Remove first entry since it's same as top level error 156 | let top_level_cause = causes.next(); 157 | if let Some(t) = top_level_cause { 158 | ret_val.push_str(&format!("{}\n\n", t)); 159 | } 160 | 161 | for cause in causes { 162 | ret_val.push_str(&format!("{}\n\n", cause)); 163 | } 164 | ret_val 165 | } 166 | 167 | fn handle_self_update_temp(cleanup_path: &Path) -> Result<()> { 168 | #[cfg(not(target_os = "linux"))] 169 | let current_bin = env::current_exe()?; 170 | 171 | #[cfg(target_os = "linux")] 172 | let current_bin = 173 | PathBuf::from(env::var("APPIMAGE").context("error getting APPIMAGE env variable")?); 174 | 175 | // Fix for self updating pre 0.5.4 to >= 0.5.4 176 | // 177 | // Pre 0.5.4, `cleanup_path` is actually the file name of the main bin name that 178 | // got passed via the CLI in the self update process. We want to rename the 179 | // current bin to that bin name. This was passed as a string of just the file 180 | // name, so we want to make an actual full path out of it first. 181 | if current_bin 182 | .file_name() 183 | .unwrap_or_default() 184 | .to_str() 185 | .unwrap_or_default() 186 | .starts_with("tmp_") 187 | { 188 | let main_bin_name = cleanup_path; 189 | 190 | let parent_dir = current_bin.parent().unwrap(); 191 | 192 | let main_bin = parent_dir.join(&main_bin_name); 193 | 194 | rename(¤t_bin, &main_bin)?; 195 | } else { 196 | remove_file(cleanup_path)?; 197 | } 198 | 199 | log::debug!("Grin GUI updated successfully"); 200 | 201 | Ok(()) 202 | } 203 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | use std::path::PathBuf; 3 | 4 | use grin_gui_core::fs::PersistentData; 5 | use serde::{Deserialize, Serialize}; 6 | use winapi::{ 7 | shared::winerror::WAIT_TIMEOUT, 8 | um::{ 9 | processthreadsapi::{GetCurrentProcess, GetCurrentProcessId, OpenProcess}, 10 | synchapi::WaitForSingleObject, 11 | winbase::QueryFullProcessImageNameW, 12 | winnt::{PROCESS_QUERY_LIMITED_INFORMATION, SYNCHRONIZE}, 13 | }, 14 | }; 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | struct Process { 18 | pid: u32, 19 | name: String, 20 | } 21 | 22 | impl PersistentData for Process { 23 | fn relative_path() -> PathBuf { 24 | PathBuf::from("pid") 25 | } 26 | } 27 | 28 | pub fn avoid_multiple_instances() { 29 | if process_already_running() { 30 | log::info!("Another instance of Grin GUI is already running. Exiting..."); 31 | std::process::exit(0); 32 | } else { 33 | // Otherwise this is the only instance. Save info about this process to the 34 | // pid file so future launches of Grin GUI can detect this running process. 35 | save_current_process_file(); 36 | } 37 | } 38 | 39 | fn process_already_running() -> bool { 40 | let old_process = if let Ok(process) = Process::load() { 41 | process 42 | } else { 43 | return false; 44 | }; 45 | 46 | unsafe { 47 | let current_pid = GetCurrentProcessId(); 48 | 49 | // In case new process somehow got recycled PID of old process 50 | if current_pid == old_process.pid { 51 | return false; 52 | } 53 | 54 | let handle = OpenProcess( 55 | SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, 56 | 0, 57 | old_process.pid, 58 | ); 59 | 60 | if let Some(name) = get_process_name(handle) { 61 | if name == old_process.name { 62 | let status = WaitForSingleObject(handle, 0); 63 | 64 | return status == WAIT_TIMEOUT; 65 | } 66 | } 67 | } 68 | 69 | false 70 | } 71 | 72 | fn save_current_process_file() { 73 | unsafe { 74 | let handle = GetCurrentProcess(); 75 | let pid = GetCurrentProcessId(); 76 | 77 | if let Some(name) = get_process_name(handle) { 78 | let process = Process { pid, name }; 79 | 80 | let _ = process.save(); 81 | } 82 | } 83 | } 84 | 85 | unsafe fn get_process_name(handle: *mut c_void) -> Option { 86 | let mut size = 256; 87 | let mut buffer = [0u16; 256]; 88 | 89 | let status = QueryFullProcessImageNameW(handle, 0, buffer.as_mut_ptr(), &mut size); 90 | 91 | if status != 0 { 92 | String::from_utf16(&buffer[..(size as usize).min(buffer.len())]).ok() 93 | } else { 94 | None 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/tray/autostart.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::mem; 4 | use std::ptr; 5 | 6 | use anyhow::Error; 7 | use grin_gui_core::fs::CONFIG_DIR; 8 | use winapi::shared::minwindef::HKEY; 9 | use winapi::um::winnt::{KEY_SET_VALUE, REG_OPTION_NON_VOLATILE, REG_SZ}; 10 | use winapi::um::winreg::{RegCreateKeyExW, RegDeleteKeyValueW, RegSetValueExW, HKEY_CURRENT_USER}; 11 | 12 | use crate::str_to_wide; 13 | 14 | pub unsafe fn toggle_autostart(enabled: bool) -> Result<(), Error> { 15 | let mut app_path = CONFIG_DIR.lock().unwrap().to_owned(); 16 | app_path.push("grin-gui.exe"); 17 | 18 | // Copy Grin GUI to config directory so we can autostart it from there 19 | let current_path = env::current_exe()?; 20 | if current_path != app_path && enabled { 21 | fs::copy(current_path, &app_path)?; 22 | } 23 | 24 | let app_path = str_to_wide!(app_path.to_str().unwrap_or_default()); 25 | let mut key_name = str_to_wide!("Software\\Microsoft\\Windows\\CurrentVersion\\Run"); 26 | let mut value_name = str_to_wide!("grin-gui"); 27 | 28 | let mut key: HKEY = mem::zeroed(); 29 | 30 | if enabled { 31 | if RegCreateKeyExW( 32 | HKEY_CURRENT_USER, 33 | key_name.as_mut_ptr(), 34 | 0, 35 | ptr::null_mut(), 36 | REG_OPTION_NON_VOLATILE, 37 | KEY_SET_VALUE, 38 | ptr::null_mut(), 39 | &mut key, 40 | ptr::null_mut(), 41 | ) == 0 42 | { 43 | RegSetValueExW( 44 | key, 45 | value_name.as_mut_ptr(), 46 | 0, 47 | REG_SZ, 48 | app_path.as_ptr() as _, 49 | app_path.len() as u32 * 2, 50 | ); 51 | } 52 | } else { 53 | RegDeleteKeyValueW( 54 | HKEY_CURRENT_USER, 55 | key_name.as_mut_ptr(), 56 | value_name.as_mut_ptr(), 57 | ); 58 | } 59 | 60 | Ok(()) 61 | } 62 | --------------------------------------------------------------------------------