Main screen redesign

Removed dark mode styling and made it all dark.

Fix large button styling on navigation

Receive page styling

Startup pages redesign

Updating field stylings.
Fix value display in recieve

Updated footer.

Added service node page.

Added wallet settings.

Added disable prop to loki field.

Update settings page.
Added merging config with default daemon option incase user provides invalid port (empty, null, etc...)

Removed theme selection

Update wallet-select pages

Fixed converting numbers to string

Update layout on address page

Added loki logo.
Made header a bit smaller.

Updated wallet init styling.
Highlight primary address in receive.

updated packages.

Updated transaction styling.

Simpler tx json handling.

Added address validation

Fixed up wallet restoration

Default node to remote.
Added drop down button to the remote node input instead of having it as a seperate field.

Removed review page.
Center align welcome page.

Replaced ryo wallet images with loki image.

Updated transaction styling.

Fix wallet errors only showing once which causes the next error to just show the loading overlay.

Added staking

Fix up status display in footer.
remove is_ready as lokid doesn't return it.

Fixed balance display in receive.
Center unlock in wallet details.

Updated README
other updates.
This commit is contained in:
Mikunj 2019-02-22 14:46:15 +11:00
parent 7b761877d1
commit bcf21c3804
44 changed files with 3110 additions and 1922 deletions

View file

@ -1,71 +1,19 @@
![Ryo Wallet](https://ryo-currency.com/img/ryo-wallet-screenshots/ryo-wallet.png) Loki GUI wallet
Next Generation GUI Wallet for Ryo-currency
---
Meet Atom, the new Electron based Ryo Wallet. Being the foundation for further development, this initial release already brings several improvements over previous GUI wallet.
- Wallet switch option.
You can keep several Ryo wallets on one PC, and switch between them easily - just pick your Ryo wallet from the list and enter your password.
- Wallet naming and identifying
Easily identify your wallets - you can give names to them, and each wallet has its own unique identicon image.
- Mixed sync. logic
We took best from both ways of sync: remote (speed) and local (reliability). At wallet startup, you connect to remote node granting quicker operation state. At the same time, you download blockchain files on your hard drive. It will add more reliability to wallet operation. If remote connection fails, you will have local node running. You can also choose to run as normal full node or lite option.
- Power user settings
You can rely on predefined optimum settings, or you can edit settings (list will be expanded):
- Sync. switch (mixed/local/remote)
- Lmdb storage path
- Various ports (daemon, p2p, ryod, remote etc)
- Remote node URL
- Bandwidth utilization (upload/download speed)
- Improved address book
Adding recipients into your address book will let you keep track of who you have sent funds to - you can add recipients of your payments beforehand, or after transactions. Seamless Ryo address validation of fields is built into the address book.
- Lazy load tx history tab
Scroll down and check your transactions list without pagination
- Interface updates
Resizable window with various UX improvements over previous version
- Increased stability and response time
Known issue with stuck processes after closing GUI wallet is now a past history. Overall increased speed and reduced response time of wallet's interface.
- Non latin seed restore
Restore your wallet with non-latin characters (Russian, German, Chinese and other languages)
- Import wallet from old GUI
Ryo wallet will scan default folders used by Lite wallet and GUI wallet and will give ability to restore from key files.
---
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/01-initialize.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/02_wallet-select-1-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/03_wallet-select-2-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/04_wallet-main-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/05_wallet-receive-1-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/06_wallet-receive-2-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/07_wallet-send-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/08_wallet-address-book-1-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/09_wallet-address-book-2.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/10_wallet-address-book-3-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/11_tx-history-light.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/12_switch-wallet-light.png)
--- ---
### Building from source ### Building from source
#### Pre-requisite
- Download latest [Lokid](https://github.com/loki-project/loki/releases)
#### Commands
``` ```
npm install -g quasar-cli npm install -g quasar-cli
git clone https://github.com/ryo-currency/ryo-wallet git clone https://github.com/loki-project/loki-electron-wallet
cd ryo-wallet cd loki-electron-wallet
cp /path/to/ryo/binaries/ryod bin/ cp path_to_loki_binaries/lokid bin/
cp /path/to/ryo/binaries/ryo-wallet-rpc bin/ cp path_to_loki_binaries/loki-wallet-rpc bin/
npm install npm install
quasar build -m electron -t mat quasar build -m electron -t mat
``` ```

1249
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"daemonVersion": "0.3.2.0", "daemonVersion": "0.3.2.0",
"description": "Modern GUI interface for Loki Currency", "description": "Modern GUI interface for Loki Currency",
"productName": "Loki Wallet Atom", "productName": "Loki Wallet Atom",
"cordovaId": "com.ryo-currency.ryo-gui-wallet", "cordovaId": "com.lokinetwork.wallet",
"author": "Loki", "author": "Loki",
"private": true, "private": true,
"scripts": { "scripts": {
@ -44,8 +44,8 @@
"eslint-plugin-promise": "^3.7.0", "eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.3.0", "eslint-plugin-vue": "^4.3.0",
"node-sass": "^4.9.3", "node-sass": "^4.11.0",
"quasar-cli": "^0.17.23", "quasar-cli": "^0.17.24",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"strip-ansi": "^3.0.1" "strip-ansi": "^3.0.1"
}, },

View file

@ -89,7 +89,8 @@ module.exports = function (ctx) {
"QInnerLoading", "QInnerLoading",
"QInfiniteScroll", "QInfiniteScroll",
"QDatetime", "QDatetime",
"QContextMenu" "QContextMenu",
"QScrollArea"
], ],
directives: [ directives: [
"Ripple", "Ripple",
@ -117,7 +118,7 @@ module.exports = function (ctx) {
display: "standalone", display: "standalone",
orientation: "portrait", orientation: "portrait",
background_color: "#ffffff", background_color: "#ffffff",
theme_color: "#027be3", theme_color: "#43BD43",
icons: [{ icons: [{
"src": "statics/icons/icon-128x128.png", "src": "statics/icons/icon-128x128.png",
"sizes": "128x128", "sizes": "128x128",

View file

@ -7,6 +7,7 @@ const WebSocket = require("ws")
const os = require("os") const os = require("os")
const fs = require("fs") const fs = require("fs")
const path = require("path") const path = require("path")
const objectAssignDeep = require("object-assign-deep")
export class Backend { export class Backend {
constructor (mainWindow) { constructor (mainWindow) {
@ -38,7 +39,7 @@ export class Backend {
this.config_file = path.join(this.config_dir, "gui", "config.json") this.config_file = path.join(this.config_dir, "gui", "config.json")
const daemon = { const daemon = {
type: "local_remote", type: "remote",
p2p_bind_ip: "0.0.0.0", p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 22022, p2p_bind_port: 22022,
rpc_bind_ip: "127.0.0.1", rpc_bind_ip: "127.0.0.1",
@ -52,44 +53,50 @@ export class Backend {
log_level: 0 log_level: 0
} }
this.config_data = { const daemons = {
main: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
staging: {
...daemon,
type: "local",
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
test: {
...daemon,
type: "local",
p2p_bind_port: 38156,
rpc_bind_port: 38157,
zmq_rpc_bind_port: 38158
}
}
// Default values
this.defaults = {
daemons: objectAssignDeep({}, daemons),
app: { app: {
data_dir: this.config_dir, data_dir: this.config_dir,
ws_bind_port: 12213, ws_bind_port: 12213,
net_type: "main" net_type: "main"
}, },
appearance: {
theme: "light"
},
daemons: {
main: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
staging: {
...daemon,
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
test: {
...daemon,
p2p_bind_port: 38156,
rpc_bind_port: 38157,
zmq_rpc_bind_port: 38158
}
},
wallet: { wallet: {
rpc_bind_port: 18082, rpc_bind_port: 18082,
log_level: 0 log_level: 0
} }
} }
this.config_data = {
// Copy all the properties of defaults
...objectAssignDeep({}, this.defaults),
appearance: {
theme: "dark"
}
}
this.remotes = [ this.remotes = [
{ {
host: "doopool.xyz", host: "doopool.xyz",
@ -195,6 +202,21 @@ export class Backend {
Object.keys(params).map(key => { Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key]) this.config_data[key] = Object.assign(this.config_data[key], params[key])
}) })
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Validate deamon data
this.config_data = {
...this.config_data,
...validated,
}
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => { fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
if (data.method == "save_config_init") { if (data.method == "save_config_init") {
this.startup() this.startup()
@ -244,7 +266,8 @@ export class Backend {
startup () { startup () {
this.send("set_app_data", { this.send("set_app_data", {
remotes: this.remotes remotes: this.remotes,
defaults: this.defaults
}) })
fs.readFile(this.config_file, "utf8", (err, data) => { fs.readFile(this.config_file, "utf8", (err, data) => {
@ -269,6 +292,19 @@ export class Backend {
// here we may want to check if config data is valid, if not also send code -1 // here we may want to check if config data is valid, if not also send code -1
// i.e. check ports are integers and > 1024, check that data dir path exists, etc // i.e. check ports are integers and > 1024, check that data dir path exists, etc
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Make sure the daemon data is valid
this.config_data = {
...this.config_data,
...validated
}
// save config file back to file, so updated options are stored on disk // save config file back to file, so updated options are stored on disk
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8") fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8")
@ -384,4 +420,37 @@ export class Backend {
}) })
}) })
} }
// Replace any invalid value with default values
validate_values (values, defaults) {
const isDictionary = (v) => typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date);
const modified = { ...values }
// Make sure we have valid defaults
if (!isDictionary(defaults)) return modified
for (const key in modified) {
// Only modify if we have a default
if (!(key in defaults)) continue
const defaultValue = defaults[key]
const invalidDefault = defaultValue === null || defaultValue === undefined || Number.isNaN(defaultValue)
if (invalidDefault) continue
const value = modified[key]
// If we have a object then recurse through it
if (isDictionary(value)) {
modified[key] = this.validate_values(value, defaultValue)
} else {
// Check if we need to replace the value
const isValidValue = !(value === undefined || value === null || value === "" || Number.isNaN(value))
if (isValidValue) continue
// Otherwise set the default value
modified[key] = defaultValue
}
}
return modified
}
} }

View file

@ -160,6 +160,10 @@ export class WalletRPC {
let params = data.data let params = data.data
switch (data.method) { switch (data.method) {
case "validate_address":
this.validateAddress(params.address)
break
case "list_wallets": case "list_wallets":
this.listWallets() this.listWallets()
break break
@ -191,6 +195,10 @@ export class WalletRPC {
this.closeWallet() this.closeWallet()
break break
case "stake":
this.stake(params.password, params.amount, params.key, params.destination)
break
case "transfer": case "transfer":
this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book) this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book)
break break
@ -238,7 +246,39 @@ export class WalletRPC {
} }
} }
validateAddress (address) {
this.sendRPC("validate_address", {
address
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_valid_address", {
address,
valid: false
})
return
}
const { valid, nettype } = data.result
// Check if the net types match
let ourNetType = "mainnet"
if (this.net_type === "test") ourNetType = "testnet"
if (this.net_type === "staging") ourNetType = "stagenet"
const netMatches = ourNetType === nettype
const isValid = valid && netMatches
this.sendGateway("set_valid_address", {
address,
valid: isValid,
nettype
})
})
}
createWallet (filename, password, language) { createWallet (filename, password, language) {
// Reset the status error
this.sendGateway("reset_wallet_error")
this.sendRPC("create_wallet", { this.sendRPC("create_wallet", {
filename, filename,
password, password,
@ -265,6 +305,7 @@ export class WalletRPC {
let timestamp = refresh_start_timestamp_or_height let timestamp = refresh_start_timestamp_or_height
timestamp = timestamp - (timestamp % 86400000) - 86400000 timestamp = timestamp - (timestamp % 86400000) - 86400000
this.sendGateway("reset_wallet_error")
this.backend.daemon.timestampToHeight(timestamp).then((height) => { this.backend.daemon.timestampToHeight(timestamp).then((height) => {
if (height === false) { this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid restore date"}}) } else { this.restoreWallet(filename, password, seed, "height", height) } if (height === false) { this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid restore date"}}) } else { this.restoreWallet(filename, password, seed, "height", height) }
}) })
@ -278,6 +319,7 @@ export class WalletRPC {
} }
seed = seed.trim().replace(/\s{2,}/g, " ") seed = seed.trim().replace(/\s{2,}/g, " ")
this.sendGateway("reset_wallet_error")
this.sendRPC("restore_deterministic_wallet", { this.sendRPC("restore_deterministic_wallet", {
filename, filename,
password, password,
@ -289,25 +331,12 @@ export class WalletRPC {
return return
} }
// restore wallet rpc does not automatically open the wallet after restoring
// ^ above behavior is now fixed, no need to open wallet manually
// this.sendRPC("open_wallet", {
// filename,
// password
// }).then((data) => {
// if(data.hasOwnProperty("error")) {
// this.sendGateway("set_wallet_error", {status:data.error})
// return
// }
// store hash of the password so we can check against it later when requesting private keys, or for sending txs // store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex") this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename this.wallet_state.name = filename
this.wallet_state.open = true this.wallet_state.open = true
this.finalizeNewWallet(filename) this.finalizeNewWallet(filename)
// });
}) })
} }
@ -352,6 +381,9 @@ export class WalletRPC {
} }
importWallet (filename, password, import_path) { importWallet (filename, password, import_path) {
// Reset the status error
this.sendGateway("reset_wallet_error")
// trim off suffix if exists // trim off suffix if exists
if (import_path.endsWith(".keys")) { if (import_path.endsWith(".keys")) {
import_path = import_path.substring(0, import_path.length - ".keys".length) import_path = import_path.substring(0, import_path.length - ".keys".length)
@ -380,8 +412,8 @@ export class WalletRPC {
password password
}).then((data) => { }).then((data) => {
if (data.hasOwnProperty("error")) { if (data.hasOwnProperty("error")) {
fs.unlinkSync(destination) if (fs.existsSync(destination)) fs.unlinkSync(destination)
fs.unlinkSync(destination + ".keys") if (fs.existsSync(destination + ".keys")) fs.unlinkSync(destination + ".keys")
this.sendGateway("set_wallet_error", {status: data.error}) this.sendGateway("set_wallet_error", {status: data.error})
return return
@ -393,6 +425,8 @@ export class WalletRPC {
this.wallet_state.open = true this.wallet_state.open = true
this.finalizeNewWallet(filename) this.finalizeNewWallet(filename)
}).catch(() => {
this.sendGateway("set_wallet_error", {status: {code: -1, message: "An unknown error occured"}})
}) })
} }
} }
@ -460,6 +494,7 @@ export class WalletRPC {
} }
openWallet (filename, password) { openWallet (filename, password) {
this.sendGateway("reset_wallet_error")
this.sendRPC("open_wallet", { this.sendRPC("open_wallet", {
filename, filename,
password password
@ -582,6 +617,51 @@ export class WalletRPC {
}) })
} }
stake (password, amount, service_node_key, destination) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("set_stake_status", {
code: -1,
message: "Internal error",
sending: false
})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("set_stake_status", {
code: -1,
message: "Invalid password",
sending: false
})
return
}
amount = parseFloat(amount).toFixed(9) * 1e9
this.sendRPC("stake", {
amount,
destination,
service_node_key
}).then((data) => {
if (data.hasOwnProperty("error")) {
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1)
this.sendGateway("set_stake_status", {
code: -1,
message: error,
sending: false
})
return
}
this.sendGateway("set_stake_status", {
code: 0,
message: "Successfully staked",
sending: false
})
})
})
}
transfer (password, amount, address, payment_id, priority, address_book = {}) { transfer (password, amount, address, payment_id, priority, address_book = {}) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) { if (err) {
@ -811,11 +891,12 @@ export class WalletRPC {
} }
} }
if (data.result.hasOwnProperty("in")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.in) } const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov"]
if (data.result.hasOwnProperty("out")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.out) } types.forEach(type => {
if (data.result.hasOwnProperty("pending")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pending) } if (data.result.hasOwnProperty(type)) {
if (data.result.hasOwnProperty("failed")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.failed) } wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result[type]);
if (data.result.hasOwnProperty("pool")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pool) } }
})
for (let i = 0; i < wallet.transactions.tx_list.length; i++) { for (let i = 0; i < wallet.transactions.tx_list.length; i++) {
if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) { if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) {

View file

@ -15,72 +15,52 @@
<q-btn class="q-ml-sm" color="primary" @click="save()" label="Save" /> <q-btn class="q-ml-sm" color="primary" @click="save()" label="Save" />
</q-toolbar> </q-toolbar>
<div> <div class="address-book-modal q-mx-md">
<LokiField label="Address" :error="$v.newEntry.address.$error">
<q-input
v-model="newEntry.address"
:placeholder="address_placeholder"
@blur="$v.newEntry.address.$touch"
:dark="theme=='dark'"
hide-underline
/>
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
dark
/>
</LokiField>
<LokiField label="Name">
<q-input
v-model="newEntry.name"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField label="Payment ID" :error="$v.newEntry.payment_id.$error" optional>
<q-input
v-model="newEntry.payment_id"
placeholder="16 or 64 hexadecimal characters"
@blur="$v.newEntry.payment_id.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField label="Notes" optional>
<q-input
v-model="newEntry.description"
placeholder="Additional notes"
type="textarea"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-list no-border :dark="theme=='dark'"> <q-field v-if="mode=='edit'">
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
<q-item> </q-field>
<q-item-side class="self-start">
<Identicon :address="newEntry.address" menu />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newEntry.address" float-label="Address"
@blur="$v.newEntry.address.$touch"
:error="$v.newEntry.address.$error"
:dark="theme=='dark'"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.name" float-label="Name" :dark="theme=='dark'" />
</q-field>
</q-item-main>
<q-item-side class="self-start q-pa-sm">
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
/>
</q-item-side>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.payment_id" float-label="Payment ID (optional)"
@blur="$v.newEntry.payment_id.$touch"
:error="$v.newEntry.payment_id.$error"
:dark="theme=='dark'"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.description" type="textarea" float-label="Notes (optional)" :dark="theme=='dark'" />
</q-field>
</q-item-main>
</q-item>
<q-item v-if="mode=='edit'">
<q-item-main>
<q-field>
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
</q-field>
</q-item-main>
</q-item>
</q-list>
</div> </div>
</q-modal-layout> </q-modal-layout>
@ -119,7 +99,7 @@
<span class="vertical-middle q-ml-xs">Recent transactions with this address</span> <span class="vertical-middle q-ml-xs">Recent transactions with this address</span>
</div> </div>
<TxList type="in" :limit="5" :to-outgoing-address="entry.address" /> <TxList type="all_in" :limit="5" :to-outgoing-address="entry.address" :key="entry.address"/>
</div> </div>
@ -137,6 +117,7 @@ import { mapState } from "vuex"
import Identicon from "components/identicon" import Identicon from "components/identicon"
import AddressHeader from "components/address_header" import AddressHeader from "components/address_header"
import TxList from "components/tx_list" import TxList from "components/tx_list"
import LokiField from "components/loki_field"
import { payment_id, address } from "src/validators/common" import { payment_id, address } from "src/validators/common"
import { required } from "vuelidate/lib/validators" import { required } from "vuelidate/lib/validators"
export default { export default {
@ -161,11 +142,27 @@ export default {
view_only: state => state.gateway.wallet.info.view_only, view_only: state => state.gateway.wallet.info.view_only,
is_ready (state) { is_ready (state) {
return this.$store.getters["gateway/isReady"] return this.$store.getters["gateway/isReady"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
} }
}), }),
validations: { validations: {
newEntry: { newEntry: {
address: { required, address }, address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
payment_id: { payment_id } payment_id: { payment_id }
} }
}, },
@ -235,7 +232,8 @@ export default {
components: { components: {
AddressHeader, AddressHeader,
Identicon, Identicon,
TxList TxList,
LokiField
} }
} }
</script> </script>
@ -243,12 +241,14 @@ export default {
<style lang="scss"> <style lang="scss">
.address-book-details { .address-book-details {
.q-field { .address-book-modal {
margin: 0 10px 20px; > .loki-field {
} margin-top: 16px;
.q-checkbox.star-entry .q-checkbox-icon { }
font-size:40px;
margin-left: 10px; .star-entry {
padding: 4px;
}
} }
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}"> <q-modal v-model="isVisible" maximized>
<q-modal-layout> <q-modal-layout>
<q-toolbar slot="header" color="dark" inverted> <q-toolbar slot="header" color="dark" inverted>
<q-btn <q-btn
@ -23,6 +23,7 @@
<AddressHeader :address="address.address" <AddressHeader :address="address.address"
:title="address.address_index == 0 ? 'Primary address' : 'Sub-address (Index '+address.address_index+')'" :title="address.address_index == 0 ? 'Primary address' : 'Sub-address (Index '+address.address_index+')'"
:extra="'You have '+(address.used?'used':'not used')+' this address'" :extra="'You have '+(address.used?'used':'not used')+' this address'"
:showCopy="false"
/> />
@ -87,7 +88,7 @@
</div> </div>
<div style="margin: 0 -16px;"> <div style="margin: 0 -16px;">
<TxList type="in" :limit="5" :to-incoming-address-index="address.address_index" /> <TxList type="all_in" :limit="5" :to-incoming-address-index="address.address_index" :key="address.address"/>
</div> </div>
</div> </div>

View file

@ -1,17 +1,15 @@
<template> <template>
<q-item class="address-header"> <q-item class="address-header">
<q-item-side>
<Identicon :address="address" :size="12" ref="identicon" />
</q-item-side>
<q-item-main class="self-start"> <q-item-main class="self-start">
<q-item-tile label>{{ title }}</q-item-tile> <q-item-tile sublabel class="title">{{ title }}</q-item-tile>
<q-item-tile class="monospace break-all" sublabel>{{ address }}</q-item-tile> <q-item-tile class="break-all" label>{{ address }}</q-item-tile>
<q-item-tile v-if="payment_id" sublabel>Payment id: {{ payment_id }}</q-item-tile> <q-item-tile v-if="payment_id" sublabel>Payment id: {{ payment_id }}</q-item-tile>
<q-item-tile v-if="extra" sublabel>{{ extra }}</q-item-tile> <q-item-tile v-if="extra" sublabel class="extra">{{ extra }}</q-item-tile>
</q-item-main> </q-item-main>
<q-item-side> <q-item-side v-if="showCopy">
<q-btn <q-btn
color="primary" style="width:25px;" color="primary"
style="width:25px;"
size="sm" icon="file_copy" size="sm" icon="file_copy"
ref="copy" ref="copy"
@click="copyAddress"> @click="copyAddress">
@ -28,11 +26,6 @@
@click.native="copyAddress(address, $event)"> @click.native="copyAddress(address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
@ -60,6 +53,11 @@ export default {
extra: { extra: {
type: String, type: String,
required: false required: false
},
showCopy: {
type: Boolean,
required: false,
default: true
} }
}, },
data () { data () {
@ -125,7 +123,20 @@ export default {
.q-item-main { .q-item-main {
.q-item-label { .q-item-label {
font-size:2em; font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
}
.title {
font-size: 15px;
margin-bottom: 2px;
}
.extra {
margin-top: 8px;
} }
} }
} }

View file

@ -1,21 +1,24 @@
<template> <template>
<q-layout-footer class="status-footer"> <q-layout-footer class="status-footer">
<div class="status-line"> <div class="status-line row items-center">
<div class="status row items-center">
<span>Status:</span>
<span class="status-text" :class="[status]">{{ status | upperCase }}</span>
</div>
<div class="row">
<template v-if="config_daemon.type !== 'remote'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<template v-if="config_daemon.type !== 'remote'"> <template v-if="config_daemon.type !== 'local'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div> <div>Remote: {{ daemon.info.height }}</div>
</template> </template>
<template v-if="config_daemon.type !== 'local'"> <div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
<div>Remote: {{ daemon.info.height }}</div> </div>
</template>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
<div>{{ status }}</div>
</div> </div>
<div class="status-bars"> <div class="status-bars" :class="[status]">
<div v-bind:style="{ width: daemon_pct+'%' }"></div> <div v-bind:style="{ width: daemon_pct+'%' }"></div>
<div v-bind:style="{ width: wallet_pct+'%' }"></div> <div v-bind:style="{ width: wallet_pct+'%' }"></div>
</div> </div>
@ -36,7 +39,7 @@ export default {
return this.config.daemons[this.config.app.net_type] return this.config.daemons[this.config.app.net_type]
}, },
target_height (state) { target_height (state) {
if(this.config_daemon.type === "local" && !this.daemon.info.is_ready) if(this.config_daemon.type === "local")
return Math.max(this.daemon.info.height, this.daemon.info.target_height) return Math.max(this.daemon.info.height, this.daemon.info.target_height)
else else
return this.daemon.info.height return this.daemon.info.height
@ -64,26 +67,31 @@ export default {
}, },
status(state) { status(state) {
if(this.config_daemon.type === "local") { if(this.config_daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) { if(this.daemon.info.height_without_bootstrap < this.target_height) {
return "Syncing..." return "syncing"
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) { } else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..." return "scanning"
} else { } else {
return "Ready" return "ready"
} }
} else { } else {
if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) { if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..." return "scanning"
} else if(this.daemon.info.height_without_bootstrap < this.target_height) { } else if(this.daemon.info.height_without_bootstrap < this.target_height) {
return "Syncing..." return "syncing"
} else { } else {
return "Ready" return "ready"
} }
} }
return return
} }
}), }),
filters: {
upperCase: function (status) {
return status.toUpperCase();
}
},
data () { data () {
return { return {
} }

View file

@ -0,0 +1,95 @@
<template>
<div class="loki-field" :class="{disable, 'disable-hover': disableHover}">
<div class="label row items-center" v-if="label" :disabled="disable">
{{ label }}
<span v-if="optional" class="optional">(Optional)</span>
</div>
<div class="content row items-center" :class="{error}">
<slot></slot>
</div>
<div class="error-label" v-if="error && errorLabel" :disabled="disable">{{ errorLabel }}</div>
</div>
</template>
<script>
export default {
name: "LokiField",
props: {
label: {
type: String,
required: false
},
error: {
type: Boolean,
required: false,
default: false
},
errorLabel: {
type: String,
required: false
},
optional: {
type: Boolean,
required: false,
default: false
},
disable: {
type: Boolean,
required: false,
default: false
},
disableHover: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
}
},
}
</script>
<style lang="scss">
.loki-field {
.label {
margin: 6px 0;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
// Disable text selection
-webkit-user-select: none;
user-select: none;
cursor: default;
.optional {
font-weight: 400;
margin-left: 4px;
}
}
.content {
border-radius: 3px;
padding: 6px 8px;
min-height: 46px;
> * {
margin-right: 4px;
}
> *:last-child {
margin-right: 0px;
}
.q-input, .q-select, .q-datetime-input {
flex: 1;
}
.q-btn {
padding: 4px 8px;
font-size: 12px !important;
}
}
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<q-btn class="menu" icon="menu" label="" size="md" flat> <q-btn class="menu" icon="menu" size="md" flat>
<q-popover> <q-popover>
<q-list separator link> <q-list separator link>
<q-item v-close-overlay @click.native="switchWallet" v-if="!disableSwitchWallet"> <q-item v-close-overlay @click.native="switchWallet" v-if="!disableSwitchWallet">
@ -31,7 +31,7 @@
<q-modal minimized ref="aboutModal"> <q-modal minimized ref="aboutModal">
<div class="about-modal"> <div class="about-modal">
<img class="q-mb-md" src="statics/ryo-wallet.svg" height="42" /> <img class="q-mb-md" src="statics/loki.svg" height="42" />
<p class="q-my-sm">Version: ATOM v{{version}}-v{{daemonVersion}}</p> <p class="q-my-sm">Version: ATOM v{{version}}-v{{daemonVersion}}</p>
<p class="q-my-sm">Copyright (c) 2018, Ryo Currency Project</p> <p class="q-my-sm">Copyright (c) 2018, Ryo Currency Project</p>
@ -44,7 +44,8 @@
<p> <p>
<a @click="openExternal('https://t.me/joinchat/DeNvR0JJ4JPn6TVSQjCsZQ')" href="#">Telegram</a> - <a @click="openExternal('https://t.me/joinchat/DeNvR0JJ4JPn6TVSQjCsZQ')" href="#">Telegram</a> -
<a @click="openExternal('https://discordapp.com/invite/67GXfD6')" href="#">Discord</a> - <a @click="openExternal('https://discordapp.com/invite/67GXfD6')" href="#">Discord</a> -
<a @click="openExternal('https://www.reddit.com/r/LokiProject/')" href="#">Reddit</a> <a @click="openExternal('https://www.reddit.com/r/LokiProject/')" href="#">Reddit</a> -
<a @click="openExternal('https://github.com')" href="#">Github</a>
</p> </p>
</div> </div>

View file

@ -11,6 +11,7 @@
<q-btn-toggle <q-btn-toggle
v-model="page" v-model="page"
toggle-color="primary" toggle-color="primary"
color="tertiary"
size="md" size="md"
:options="tabs" :options="tabs"
/> />
@ -25,22 +26,6 @@
</div> </div>
</div> </div>
<div v-if="page=='appearance'">
<div class="q-pa-md">
<h6 class="q-mb-md q-mt-none" style="font-weight: 300">Select Appearance:</h6>
<q-btn-toggle
v-model="theme"
toggle-color="primary"
size="md"
:options="[
{label: 'Light theme', value: 'light', icon: 'brightness_5'},
{label: 'Dark theme', value: 'dark', icon: 'brightness_2'},
]"
/>
</div>
</div>
<div v-if="page=='peers'"> <div v-if="page=='peers'">
<q-list :dark="theme=='dark'" no-border> <q-list :dark="theme=='dark'" no-border>
<q-list-header>Peer list</q-list-header> <q-list-header>Peer list</q-list-header>
@ -79,6 +64,7 @@ import SettingsGeneral from "components/settings_general"
export default { export default {
name: "SettingsModal", name: "SettingsModal",
computed: mapState({ computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
daemon: state => state.gateway.daemon, daemon: state => state.gateway.daemon,
pending_config: state => state.gateway.app.pending_config, pending_config: state => state.gateway.app.pending_config,
config: state => state.gateway.app.config, config: state => state.gateway.app.config,
@ -86,7 +72,6 @@ export default {
const { app, daemons } = state.gateway.app.config; const { app, daemons } = state.gateway.app.config;
let tabs = [ let tabs = [
{label: 'General', value: 'general', icon: 'settings'}, {label: 'General', value: 'general', icon: 'settings'},
{label: 'Appearance', value: 'appearance', icon: 'visibility'},
] ]
if(daemons[app.net_type].type != 'remote') { if(daemons[app.net_type].type != 'remote') {
tabs.push({label: 'Peers', value: 'peers', icon: 'cloud_queue'}) tabs.push({label: 'Peers', value: 'peers', icon: 'cloud_queue'})
@ -97,22 +82,10 @@ export default {
data () { data () {
return { return {
page: "general", page: "general",
theme: null,
isVisible: false isVisible: false
} }
}, },
mounted: function () {
this.theme = this.config.appearance.theme
},
watch: { watch: {
theme: function (theme, old) {
if(old == null) return
this.$gateway.send("core", "quick_save_config", {
appearance: {
theme: this.theme
}
})
},
isVisible: function () { isVisible: function () {
if(this.isVisible == false) { if(this.isVisible == false) {
this.$store.dispatch("gateway/resetPendingConfig") this.$store.dispatch("gateway/resetPendingConfig")

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="settings-general"> <div class="settings-general">
<div class="row justify-between q-mb-md"> <div class="row justify-between q-mb-md">
<div><q-radio v-model="config_daemon.type" val="remote" label="Remote Daemon Only" /></div>
<div><q-radio v-model="config_daemon.type" val="local_remote" label="Local + Remote Daemon" /></div> <div><q-radio v-model="config_daemon.type" val="local_remote" label="Local + Remote Daemon" /></div>
<div><q-radio v-model="config_daemon.type" val="local" label="Local Daemon Only" /></div> <div><q-radio v-model="config_daemon.type" val="local" label="Local Daemon Only" /></div>
<div><q-radio v-model="config_daemon.type" val="remote" label="Remote Daemon Only" /></div>
</div> </div>
<p v-if="config_daemon.type == 'local_remote'"> <p v-if="config_daemon.type == 'local_remote'">
@ -12,122 +12,131 @@
<p v-if="config_daemon.type == 'local'"> <p v-if="config_daemon.type == 'local'">
Full security, wallet will download the full blockchain. You will not be able to transact until sync is completed. Full security, wallet will download the full blockchain. You will not be able to transact until sync is completed.
</p> </p>
<p v-if="config_daemon.type == 'remote'"> <p v-if="is_remote">
Less security, wallet will connect to a remote node to make all transactions. Less security, wallet will connect to a remote node to make all transactions.
</p> </p>
<q-field v-if="config_daemon.type != 'remote'"> <template v-if="config_daemon.type != 'remote'">
<div class="row gutter-sm"> <div class="row pl-sm">
<div class="col-8"> <LokiField class="col-8" label="Local Daemon IP" disable>
<q-input v-model="config_daemon.rpc_bind_ip" float-label="Local Daemon IP" <q-input
:dark="theme=='dark'" disable /> v-model="config_daemon.rpc_bind_ip"
</div> :placeholder="daemon_defaults.rpc_bind_ip"
<div class="col-4"> :dark="theme=='dark'"
<q-input v-model="config_daemon.rpc_bind_port" float-label="Local Daemon Port (RPC)" type="number" :decimals="0" :step="1" min="1024" max="65535" :dark="theme=='dark'" /> disable
</div> hide-underline
/>
</LokiField>
<LokiField class="col-4" label="Local Daemon Port (RPC)">
<q-input
v-model="config_daemon.rpc_bind_port"
:placeholder="toString(daemon_defaults.rpc_bind_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div> </div>
</q-field> </template>
<q-field v-if="config_daemon.type != 'local'"> <template v-if="config_daemon.type != 'local'">
<div class="row gutter-sm"> <div class="row q-mt-md pl-sm">
<div class="col-8"> <LokiField class="col-8" label="Remote Node Host">
<q-input v-model="config_daemon.remote_host" float-label="Remote Node Host" :dark="theme=='dark'" /> <q-input
</div> v-model="config_daemon.remote_host"
<div class="col-4"> :placeholder="daemon_defaults.remote_host"
<q-input v-model="config_daemon.remote_port" float-label="Remote Node Port" type="number" :decimals="0" :step="1" min="1024" max="65535" :dark="theme=='dark'" /> :dark="theme=='dark'"
</div> hide-underline
/>
<!-- Remote node presets -->
<q-btn-dropdown class="remote-dropdown" v-if="config.app.net_type === 'main'" flat>
<q-list link dark no-border>
<q-item v-for="option in remotes" :key="option.host" @click.native="setPreset(option)" v-close-overlay>
<q-item-main>
<q-item-tile label>{{ option.host }}:{{ option.port }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-btn-dropdown>
</LokiField>
<LokiField class="col-4" label="Remote Node Port">
<q-input
v-model="config_daemon.remote_port"
:placeholder="toString(daemon_defaults.remote_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div> </div>
</q-field> </template>
<q-field> <div class="col q-mt-md">
<div class="row gutter-sm"> <LokiField label="Data Storage Path" disable-hover>
<div class="col-8"> <q-input v-model="config.app.data_dir" disable :dark="theme=='dark'" hide-underline/>
<q-input v-model="config.app.data_dir" stack-label="Data Storage Path" disable :dark="theme=='dark'" /> <input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInput" hidden />
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInput" hidden /> <q-btn color="secondary" v-on:click="selectPath" :text-color="theme=='dark'?'white':'dark'">Select Location</q-btn>
</div> </LokiField>
<div class="col-4">
<q-btn v-on:click="selectPath" :text-color="theme=='dark'?'white':'dark'">Select Location</q-btn>
</div>
</div>
</q-field>
<div v-if="(config_daemon.type !== 'local') && (config.app.net_type === 'main')">
<hr>
<div class="presets">
<div class="q-body-1">Remote Node Presets</div>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<q-select
v-model="select"
:options="remoteOptions"
:dark="theme=='dark'"
/>
</div>
<div class="col-4">
<q-btn v-on:click="loadPreset" :text-color="theme=='dark'?'white':'dark'">Load Preset</q-btn>
</div>
</div>
</q-field>
</div>
</div> </div>
<q-collapsible label="Advanced Options" header-class="non-selectable row reverse advanced-options-label"> <q-collapsible label="Advanced Options" header-class="q-mt-sm non-selectable row reverse advanced-options-label">
<q-field> <div class="row pl-sm q-mt-sm">
<div class="row gutter-sm"> <LokiField class="col-6" label="Daemon Log Level" :disable="is_remote">
<div class="col-6"> <q-input v-model="config_daemon.log_level" :placeholder="toString(daemon_defaults.log_level)" :disable="is_remote" :dark="theme=='dark'"
<q-input v-model="config_daemon.log_level" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
float-label="Daemon Log Level" type="number" :decimals="0" :step="1" min="0" max="4" /> </LokiField>
</div> <LokiField class="col-6" label="Wallet Log Level">
<div class="col-6"> <q-input v-model="config.wallet.log_level" :placeholder="toString(defaults.wallet.log_level)" :dark="theme=='dark'"
<q-input v-model="config.wallet.log_level" :dark="theme=='dark'" type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
float-label="Wallet Log Level" type="number" :decimals="0" :step="1" min="0" max="4" /> </LokiField>
</div> </div>
</div>
</q-field>
<q-field> <div class="row pl-sm q-mt-md">
<div class="row gutter-sm"> <LokiField class="col-3" label="Max Incoming Peers" :disable="is_remote">
<div class="col-3"> <q-input v-model="config_daemon.in_peers" :placeholder="toString(daemon_defaults.in_peers)" :disable="is_remote" :dark="theme=='dark'"
<q-input v-model="config_daemon.in_peers" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
float-label="Max Incoming Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" /> </LokiField>
</div> <LokiField class="col-3" label="Max Outgoing Peers" :disable="is_remote">
<div class="col-3"> <q-input v-model="config_daemon.out_peers" :placeholder="toString(daemon_defaults.out_peers)" :disable="is_remote" :dark="theme=='dark'"
<q-input v-model="config_daemon.out_peers" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
float-label="Max Outgoing Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" /> </LokiField>
</div> <LokiField class="col-3" label="Limit Upload Rate" :disable="is_remote">
<div class="col-3"> <q-input v-model="config_daemon.limit_rate_up" :placeholder="toString(daemon_defaults.limit_rate_up)" :disable="is_remote" :dark="theme=='dark'"
<q-input v-model="config_daemon.limit_rate_up" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
float-label="Limit Upload Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" /> </LokiField>
</div> <LokiField class="col-3" label="Limit Download Rate" :disable="is_remote">
<div class="col-3"> <q-input v-model="config_daemon.limit_rate_down" :placeholder="toString(daemon_defaults.limit_rate_down)" :disable="is_remote" :dark="theme=='dark'"
<q-input v-model="config_daemon.limit_rate_down" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
float-label="Limit Download Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" /> </LokiField>
</div> </div>
</div> <div class="row pl-sm q-mt-md">
</q-field> <LokiField class="col-3" label="Daemon P2P Port" :disable="is_remote">
<q-field> <q-input v-model="config_daemon.p2p_bind_port" :placeholder="toString(daemon_defaults.p2p_bind_port)" :disable="is_remote" :dark="theme=='dark'"
<div class="row gutter-sm"> float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
<div class="col-3"> </LokiField>
<q-input v-model="config_daemon.p2p_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" <LokiField class="col-3" label="Daemon ZMQ Port" :disable="is_remote">
float-label="Daemon P2P Port" type="number" :decimals="0" :step="1" min="1024" max="65535" /> <q-input v-model="config_daemon.zmq_rpc_bind_port" :placeholder="toString(daemon_defaults.zmq_rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
</div> float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
<div class="col-3"> </LokiField>
<q-input v-model="config_daemon.zmq_rpc_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" <LokiField class="col-3" label="Internal Wallet Port">
float-label="Daemon ZMQ Port" type="number" :decimals="0" :step="1" min="1024" max="65535" /> <q-input v-model="config.app.ws_bind_port" :placeholder="toString(defaults.app.ws_bind_port)" :dark="theme=='dark'"
</div> float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
<div class="col-3"> </LokiField>
<q-input v-model="config.app.ws_bind_port" :dark="theme=='dark'" <LokiField class="col-3" label="Wallet RPC Port" :disable="is_remote">
float-label="Internal Wallet Port" type="number" :decimals="0" :step="1" min="1024" max="65535" /> <q-input v-model="config.wallet.rpc_bind_port" :placeholder="toString(defaults.wallet.rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
</div> float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
<div class="col-3"> </LokiField>
<q-input v-model="config.wallet.rpc_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'" </div>
float-label="Wallet RPC Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
</q-field>
<q-field helper="Choose a network" label="Network" orientation="vertical"> <q-field helper="Choose a network" label="Network" orientation="vertical">
<q-option-group <q-option-group
type="radio" type="radio"
@ -146,21 +155,23 @@
<script> <script>
import { mapState } from "vuex" import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default { export default {
name: "SettingsGeneral", name: "SettingsGeneral",
computed: mapState({ computed: mapState({
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
remotes: state => state.gateway.app.remotes, remotes: state => state.gateway.app.remotes,
remoteOptions (state) {
return this.remotes.map((r, index) => ({
label: `${r.host}:${r.port}`,
value: index,
}));
},
config: state => state.gateway.app.pending_config, config: state => state.gateway.app.pending_config,
config_daemon (state) { config_daemon (state) {
return this.config.daemons[this.config.app.net_type] return this.config.daemons[this.config.app.net_type]
}, },
is_remote (state) {
return this.config_daemon.type === 'remote'
},
defaults: state => state.gateway.app.defaults,
daemon_defaults (state) {
return this.defaults.daemons[this.config.app.net_type]
}
}), }),
methods: { methods: {
selectPath () { selectPath () {
@ -169,19 +180,26 @@ export default {
setDataPath (file) { setDataPath (file) {
this.config.app.data_dir = file.target.files[0].path this.config.app.data_dir = file.target.files[0].path
}, },
loadPreset () { setPreset (option) {
if (!this.remotes || this.remotes.length === 0) return; if (!option) return;
const { host, port } = this.remotes[this.select]; const { host, port } = option;
this.config_daemon.remote_host = host; if (host) this.config_daemon.remote_host = host;
this.config_daemon.remote_port = port; if (port) this.config_daemon.remote_port = port;
}, },
toString (value) {
if (!value && typeof value !== "number") return ""
return String(value);
}
}, },
data () { data () {
return { return {
select: 0, select: 0,
} }
}, },
components: {
LokiField,
}
} }
</script> </script>
@ -203,8 +221,14 @@ export default {
padding: 0; padding: 0;
} }
.presets { .row.pl-sm {
margin-top: 20px; > * + * {
padding-left: 16px;
}
}
.remote-dropdown {
padding: 0 !important;
} }
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}"> <q-modal v-model="isVisible" maximized>
<q-modal-layout> <q-modal-layout>
<q-toolbar slot="header" color="dark" inverted> <q-toolbar slot="header" color="dark" inverted>
<q-btn <q-btn
@ -85,9 +85,6 @@
<q-list no-border> <q-list no-border>
<q-list-header class="q-px-none">Incoming transaction sent to:</q-list-header> <q-list-header class="q-px-none">Incoming transaction sent to:</q-list-header>
<q-item class="q-px-none"> <q-item class="q-px-none">
<q-item-side>
<Identicon :address="in_tx_address_used.address" ref="identicon" />
</q-item-side>
<q-item-main> <q-item-main>
<q-item-tile label>{{ in_tx_address_used.address_index_text }}</q-item-tile> <q-item-tile label>{{ in_tx_address_used.address_index_text }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ in_tx_address_used.address }}</q-item-tile> <q-item-tile class="monospace ellipsis" sublabel>{{ in_tx_address_used.address }}</q-item-tile>
@ -99,11 +96,6 @@
@click.native="copyAddress(in_tx_address_used.address, $event)"> @click.native="copyAddress(in_tx_address_used.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
@ -116,26 +108,17 @@
<q-list-header class="q-px-none">Outgoing transaction sent to:</q-list-header> <q-list-header class="q-px-none">Outgoing transaction sent to:</q-list-header>
<template v-if="out_destinations"> <template v-if="out_destinations">
<q-item class="q-px-none" v-for="destination in out_destinations"> <q-item class="q-px-none" v-for="destination in out_destinations">
<q-item-side>
<Identicon :address="destination.address" ref="identicon" />
</q-item-side>
<q-item-main> <q-item-main>
<q-item-tile label>{{ destination.name }}</q-item-tile> <q-item-tile label>{{ destination.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ destination.address }}</q-item-tile> <q-item-tile class="monospace ellipsis" sublabel>{{ destination.address }}</q-item-tile>
<q-item-tile sublabel><FormatLoki :amount="destination.amount" /></q-item-tile> <q-item-tile sublabel><FormatLoki :amount="destination.amount" /></q-item-tile>
</q-item-main> </q-item-main>
<q-context-menu> <q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;"> <q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay <q-item v-close-overlay
@click.native="copyAddress(destination.address, $event)"> @click.native="copyAddress(destination.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
@ -143,9 +126,6 @@
</template> </template>
<template v-else> <template v-else>
<q-item class="q-px-none"> <q-item class="q-px-none">
<q-item-side>
<Identicon address="" />
</q-item-side>
<q-item-main> <q-item-main>
<q-item-tile label>Destination unknown</q-item-tile> <q-item-tile label>Destination unknown</q-item-tile>
</q-item-main> </q-item-main>
@ -180,7 +160,6 @@ const { clipboard } = require("electron")
import { mapState } from "vuex" import { mapState } from "vuex"
import { date } from "quasar" import { date } from "quasar"
const { formatDate } = date const { formatDate } = date
import Identicon from "components/identicon"
import TxTypeIcon from "components/tx_type_icon" import TxTypeIcon from "components/tx_type_icon"
import FormatLoki from "components/format_loki" import FormatLoki from "components/format_loki"
export default { export default {
@ -296,7 +275,6 @@ export default {
} }
}, },
components: { components: {
Identicon,
TxTypeIcon, TxTypeIcon,
FormatLoki FormatLoki
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="tx-list">
<template v-if="tx_list_paged.length === 0"> <template v-if="tx_list_paged.length === 0">
<p class="q-pa-md q-mb-none">No transactions found</p> <p class="q-pa-md q-mb-none">No transactions found</p>
@ -8,23 +8,22 @@
<template v-else> <template v-else>
<q-infinite-scroll :handler="loadMore" ref="scroller"> <q-infinite-scroll :handler="loadMore" ref="scroller">
<q-list link no-border :dark="theme=='dark'" class="tx-list"> <q-list link no-border :dark="theme=='dark'" class="tx-list">
<q-item v-for="(tx, index) in tx_list_paged" :key="tx.txid" <q-item class="transaction" v-for="(tx, index) in tx_list_paged" :key="tx.txid"
@click.native="details(tx)" :class="'tx-'+tx.type"> @click.native="details(tx)" :class="'tx-'+tx.type">
<q-item-side> <q-item-side class="type">
<TxTypeIcon :type="tx.type" /> <div>{{ tx.type | typeToString }}</div>
</q-item-side> </q-item-side>
<q-item-main> <q-item-main class="main">
<q-item-tile class="monospace ellipsis" label>{{ tx.txid }}</q-item-tile> <q-item-tile class="amount" label>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-tile label>
<FormatLoki :amount="tx.amount" /> <FormatLoki :amount="tx.amount" />
</q-item-tile> </q-item-tile>
<q-item-tile sublabel> <q-item-tile sublabel>{{ tx.txid }}</q-item-tile>
<timeago :datetime="tx.timestamp*1000" :auto-update="60"> </q-item-main>
</timeago> <q-item-side class="meta">
<q-item-tile label>
<timeago :datetime="tx.timestamp*1000" :auto-update="60" />
</q-item-tile> </q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-side> </q-item-side>
<q-context-menu> <q-context-menu>
@ -105,7 +104,7 @@ export default {
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
current_height: state => state.gateway.daemon.info.height, current_height: state => state.gateway.daemon.info.height,
wallet_height: state => state.gateway.wallet.info.height, wallet_height: state => state.gateway.wallet.info.height,
tx_list: state => state.gateway.wallet.transactions.tx_list tx_list: state => state.gateway.wallet.transactions.tx_list,
}), }),
created () { created () {
this.filterTxList() this.filterTxList()
@ -153,11 +152,50 @@ export default {
} }
}, },
}, },
filters: {
typeToString: function (value) {
switch (value) {
case "in":
return "Received"
case "out":
return "Sent"
case "failed":
return "Failed"
case "pending":
case "pool":
return "Pending"
case "miner":
return "Miner"
case "snode":
return "Service Node"
case "gov":
return "Governance"
default:
return "-"
}
}
},
methods: { methods: {
filterTxList () { filterTxList () {
const all_in = ['in', 'pool', "miner", "snode", "gov"]
const all_out = ['out', 'pending']
const all_pending = ['pending', 'pool']
this.tx_list_filtered = this.tx_list.filter((tx) => { this.tx_list_filtered = this.tx_list.filter((tx) => {
let valid = true let valid = true
if(this.type !== "all" && this.type !== tx.type) {
if (this.type === "all_in" && !all_in.includes(tx.type)) {
return false
}
if (this.type === "all_out" && !all_out.includes(tx.type)) {
return false
}
if (this.type === "all_pending" && !all_pending.includes(tx.type)) {
return false
}
if(!this.type.startsWith("all") && this.type !== tx.type) {
valid = false valid = false
return valid return valid
} }
@ -242,4 +280,41 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.tx-list {
.transaction {
margin: 0 16px;
padding: 0;
border-radius: 3px;
> * {
margin-top: 8px;
margin-bottom: 8px;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
+ .transaction {
margin-top: 10px;
}
.main {
margin: 0;
padding: 8px 10px;
}
.type {
div {
min-width: 100px;
margin-right: 8px;
}
}
}
}
</style> </style>

View file

@ -0,0 +1,106 @@
<template>
<div class="column wallet-info">
<div class="row justify-between items-center wallet-header loki-green">
<div class="title">{{ info.name }}</div>
<WalletSettings />
</div>
<div class="wallet-content">
<div class="row justify-center">
<div class="funds column items-center">
<div class="balance">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatLoki :amount="info.balance" /></span></div>
</div>
<div class="row unlocked">
<span>Unlocked: <FormatLoki :amount="info.unlocked_balance" /></span>
</div>
</div>
</div>
<div class="wallet-address row justify-center items-center">
<div class="address">{{ info.address }}</div>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
<q-tooltip anchor="center right" self="center left" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</div>
</div>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import FormatLoki from "components/format_loki"
import WalletSettings from "components/wallet_settings"
export default {
name: "WalletDetails",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
methods: {
copyAddress () {
this.$refs.copy.$el.blur()
clipboard.writeText(this.info.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
},
},
components: {
FormatLoki,
WalletSettings
},
}
</script>
<style lang="scss">
.wallet-info {
.wallet-header {
padding: 0.8rem 1.5rem;
.title {
font-weight: bold;
}
}
.wallet-content {
text-align: center;
background-color: #0A0A0A;
padding: 2em;
.balance {
.text {
font-size: 16px;
}
.value {
font-size: 35px;
}
}
.wallet-address {
margin-top: 12px;
.address {
overflow: hidden;
text-overflow: ellipsis;
margin: 4px 0;
}
.q-btn {
margin-left: 8px;
}
}
.unlocked {
font-size: 14px;
font-weight: 500;
}
}
}
</style>

View file

@ -0,0 +1,516 @@
<template>
<div class="wallet-settings">
<q-btn icon-right="more_vert" label="Settings" size="md" flat>
<q-popover anchor="bottom right" self="top right">
<q-list separator link>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="getPrivateKeys()">
<q-item-main>
<q-item-tile label>Show Private Keys</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('change_password')">
<q-item-main>
<q-item-tile label>Change Password</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('rescan')">
<q-item-main>
<q-item-tile label>Rescan Wallet</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('key_image')">
<q-item-main>
<q-item-tile label>Manage Key Images</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="deleteWallet()">
<q-item-main>
<q-item-tile label>Delete Wallet</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-popover>
</q-btn>
<!-- Modals -->
<q-modal minimized v-model="modals.private_keys.visible" @hide="closePrivateKeys()">
<div class="modal-header">Show private keys</div>
<div class="q-ma-lg">
<template v-if="secret.mnemonic">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6>
<div class="row">
<div class="col">
{{ secret.mnemonic }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('mnemonic', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy seed words
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs">View key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('view_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy view key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs">Spend key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy spend key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<div class="q-mt-lg">
<q-btn
color="primary"
@click="hideModal('private_keys')"
label="Close"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.rescan.visible">
<div class="modal-header">Rescan wallet</div>
<div class="q-ma-lg">
<p>Select full rescan or rescan of spent outputs only.</p>
<div class="q-mt-lg">
<q-radio v-model="modals.rescan.type" val="full" label="Rescan full blockchain" />
</div>
<div class="q-mt-sm">
<q-radio v-model="modals.rescan.type" val="spent" label="Rescan spent outputs" />
</div>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('rescan')"
label="Close"
/>
<q-btn
color="primary"
@click="rescanWallet()"
label="Rescan"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.key_image.visible">
<div class="modal-header">{{modals.key_image.type}} key images</div>
<div class="q-ma-lg">
<div class="row q-mb-md">
<div class="q-mr-xl">
<q-radio v-model="modals.key_image.type" val="Export" label="Export" />
</div>
<div>
<q-radio v-model="modals.key_image.type" val="Import" label="Import" />
</div>
</div>
<template v-if="modals.key_image.type == 'Export'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.export_path" stack-label="Key image export directory" disable />
<input type="file" webkitdirectory directory id="keyImageExportPath" v-on:change="setKeyImageExportPath" ref="keyImageExportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageExportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<template v-if="modals.key_image.type == 'Import'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.import_path" stack-label="Key image import file" disable />
<input type="file" id="keyImageImportPath" v-on:change="setKeyImageImportPath" ref="keyImageImportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageImportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('key_image')"
label="Close"
/>
<q-btn
color="primary"
@click="doKeyImages()"
:label="modals.key_image.type"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.change_password.visible" @hide="clearChangePassword()">
<div class="modal-header">Change password</div>
<div class="q-ma-lg">
<q-field>
<q-input v-model="modals.change_password.old_password" type="password" float-label="Old Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password" type="password" float-label="New Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password_confirm" type="password" float-label="Confirm New Password" :dark="theme=='dark'" />
</q-field>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('change_password')"
label="Close"
/>
<q-btn
color="primary"
@click="doChangePassword()"
label="Change"
/>
</div>
</div>
</q-modal>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
export default {
name: "WalletSettings",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
data_dir: state => state.gateway.app.config.app.data_dir,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
data () {
return {
modals: {
private_keys: {
visible: false,
},
rescan: {
visible: false,
type: "full",
},
key_image: {
visible: false,
type: "Export",
export_path: "",
import_path: "",
},
change_password: {
visible: false,
old_password: "",
new_password: "",
new_password_confirm: "",
},
}
}
},
mounted () {
const path = require("path")
this.modals.key_image.export_path = path.join(this.data_dir, "gui")
this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export")
},
watch: {
secret: {
handler(val, old) {
if(val.view_key == old.view_key) return
switch(this.secret.view_key) {
case "":
break
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.secret.mnemonic
})
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
break
default:
this.showModal("private_keys")
break
}
},
deep: true
}
},
methods: {
showModal (which) {
if(!this.is_ready) return
this.modals[which].visible = true
},
hideModal (which) {
this.modals[which].visible = false
},
copyPrivateKey (type, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
if(this.secret[type] == null) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Error copying private key",
})
return
}
clipboard.writeText(this.secret[type])
let type_human = type.substring(0,1).toUpperCase()+type.substring(1).replace("_"," ")
this.$q.dialog({
title: "Copy "+type_human,
message: "Be careful who you send your private keys to as they control your funds.",
ok: {
label: "OK"
},
}).then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
}).catch(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
})
},
getPrivateKeys () {
if(!this.is_ready) return
this.$q.dialog({
title: "Show private keys",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "SHOW"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "get_private_keys", {password})
}).catch(() => {
})
},
closePrivateKeys () {
this.hideModal("private_keys")
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
},
rescanWallet () {
this.hideModal("rescan")
if(this.modals.rescan.type == "full") {
this.$q.dialog({
title: "Rescan wallet",
message: "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.",
ok: {
label: "RESCAN"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "rescan_blockchain")
}).catch(() => {
})
} else {
this.$gateway.send("wallet", "rescan_spent")
}
},
selectKeyImageExportPath () {
this.$refs.keyImageExportSelect.click()
},
setKeyImageExportPath (file) {
this.modals.key_image.export_path = file.target.files[0].path
},
selectKeyImageImportPath () {
this.$refs.keyImageImportSelect.click()
},
setKeyImageImportPath (file) {
this.modals.key_image.import_path = file.target.files[0].path
},
doKeyImages () {
this.hideModal("key_image")
this.$q.dialog({
title: this.modals.key_image.type + " key images",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: this.modals.key_image.type
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
if(this.modals.key_image.type == "Export")
this.$gateway.send("wallet", "export_key_images", {password: password, path: this.modals.key_image.export_path})
else if(this.modals.key_image.type == "Import")
this.$gateway.send("wallet", "import_key_images", {password: password, path: this.modals.key_image.import_path})
}).catch(() => {
})
},
doChangePassword () {
let old_password = this.modals.change_password.old_password
let new_password = this.modals.change_password.new_password
let new_password_confirm = this.modals.change_password.new_password_confirm
if(new_password == old_password) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New password must be different"
})
} else if(new_password != new_password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New passwords do not match"
})
} else {
this.hideModal("change_password")
this.$gateway.send("wallet", "change_wallet_password", {old_password, new_password})
}
},
clearChangePassword () {
this.modals.change_password.old_password = ""
this.modals.change_password.new_password = ""
this.modals.change_password.new_password_confirm = ""
},
deleteWallet () {
this.$q.dialog({
title: "Delete wallet",
message: "Are you absolutely sure you want to delete your wallet?\nMake sure you have your private keys backed up.\nTHIS PROCESS IS NOT REVERSIBLE!",
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.$q.dialog({
title: "Delete wallet",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "delete_wallet", {password})
}).catch(() => {
})
}).catch(() => {
})
}
},
}
</script>
<style lang="scss">
</style>

View file

@ -1,4 +1,5 @@
// app global css // app global css
@import '~variables'
@font-face { @font-face {
font-family: 'RobotoMono-Light'; font-family: 'RobotoMono-Light';
@ -12,16 +13,11 @@
background: #63c9f3; background: #63c9f3;
} }
:root {
--q-color-primary: #497dc6;
}
* { * {
scrollbar-track-color: #111;
scrollbar-track-color: #fff; scrollbar-arrow-color: #111;
scrollbar-arrow-color: #fff; &::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-corner { background-color: #fff;} &::-webkit-scrollbar-track-piece { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #fff;}
scrollbar-face-color: #646464; scrollbar-face-color: #646464;
scrollbar-base-color: #646464; scrollbar-base-color: #646464;
@ -81,7 +77,7 @@ footer,
} }
.q-item-sublabel { .q-item-sublabel {
color: #121212; colr: #cecece;
} }
.advanced-options-label .q-item-side-right { .advanced-options-label .q-item-side-right {
@ -89,14 +85,15 @@ footer,
text-align: left; text-align: left;
} }
.q-layout-page { .q-layout {
min-height: 0 !important; background: $loki-black-80;
color:white;
} }
.q-layout-header { .q-layout-header {
background: white; background: #0A0A0A;
box-shadow: none; box-shadow: none;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #333;
height: 48px; height: 48px;
.q-toolbar-title { .q-toolbar-title {
@ -154,7 +151,10 @@ footer,
} }
} }
}
.q-layout-page {
min-height: 0 !important;
} }
.infoBox { .infoBox {
@ -169,7 +169,7 @@ footer,
right: 10px; right: 10px;
top: 10px; top: 10px;
width: 64px; width: 64px;
color: #212529; color: #cecece;
svg { svg {
width: 60px; width: 60px;
@ -183,7 +183,7 @@ footer,
.text { .text {
font-size: 13px; font-size: 13px;
margin-top: 8px; margin-top: 8px;
color: #212529; color: #cecece;
text-transform: uppercase; text-transform: uppercase;
} }
@ -191,7 +191,7 @@ footer,
font-size: 24px; font-size: 24px;
margin-top: 4px; margin-top: 4px;
font-weight: 800; font-weight: 800;
color: #212529; color: #cecece;
font-weight:300; font-weight:300;
} }
} }
@ -213,27 +213,44 @@ footer,
background-position: center center; background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: #cb8fe1; background-color: #cb8fe1;
box-shadow: inset rgba(255, 255, 255, 0.6) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px; box-shadow: inset rgba(0, 0, 0, 0.1) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
border-radius: 2px; border-radius: 2px;
} }
.q-layout-footer.status-footer { .q-layout-footer.status-footer {
border-top: 1px solid #ccc; background: #000000;
color: rgba(255, 255, 255, 0.51);
border-top: 1px solid #333;
padding-top: 2px; padding-top: 2px;
background: #fff;
box-shadow: none; box-shadow: none;
font-size: 12px; font-size: 12px;
.status-line { .status-line {
padding: 8px;
margin-bottom: 5px; margin-bottom: 5px;
div { .status {
display: inline-block; flex: 1;
padding: 0 8px; span {
margin-right: 4px;
}
.status-text {
font-weight: bold;
text-transform: uppercase;
}
.ready {
color: $loki-green-solid;
}
.scanning, .syncing {
color: goldenrod;
}
} }
div:last-child { div {
float:right; padding: 0 8px;
} }
} }
@ -244,17 +261,27 @@ footer,
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
-webkit-transition: width 0.5s ease-out;
transition: width 0.5s ease-out; transition: width 0.5s ease-out;
} }
div:first-child { div:first-child {
background-color: goldenrod; background-color: $loki-green-dark-solid;
} }
div:last-child { div:last-child {
background-color: green; background-color: $loki-green-solid;
}
}
.status-bars.syncing, .status-bars.scanning {
div:first-child {
background-color: #bc8f1c;
} }
div:last-child {
background-color:goldenrod;
}
} }
} }
@ -262,219 +289,272 @@ footer,
.tx-list { .tx-list {
.meta {
text-align:right;
}
.q-item.tx-in, .q-item.tx-in,
.q-item.tx-pool { .q-item.tx-pool,
.q-icon { .q-item.tx-miner,
color: #333; .q-item.tx-snode,
} .q-item.tx-gov {
.q-item-label { .amount span {
color: green; color: #43bd43;
} &:before {
&>div:last-child { content: "+";
text-align:right; color: #43bd43;
&>div:first-child {
span {
color: green;
&:before {
content: "+";
color: green;
}
}
} }
} }
} }
.q-item.tx-out, .q-item.tx-out,
.q-item.tx-pending { .q-item.tx-pending {
.q-icon { .amount span {
color: #333; color: white;
} &:before {
.q-item-label { content: "-";
color: purple; font-weight: bold;
} color: white;
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: purple;
&:before {
content: "-";
color: purple;
}
}
} }
} }
} }
.q-item.tx-failed {
.amount span {
color: orangered;
}
}
} }
.q-list-dark .q-item,
.q-item-dark .q-item,
.q-list-header {
color: #cecece;
}
body.dark { .q-stepper-tab.step-waiting,
.q-stepper-step-content{
color: #cecece;
}
&, * { .q-popover {
scrollbar-track-color: #111; background: #222;
scrollbar-arrow-color: #111; color:#cecece;
&::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #111;}
}
.q-item-sublabel { .q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
color: #cecece;
}
.infoBoxIcon {
color: #cecece;
}
.infoBoxContent {
.text {
color: #cecece;
}
.value {
color: #cecece;
}
}
.identicon {
box-shadow: inset rgba(0, 0, 0, 0.1) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
border-radius: 2px;
}
.q-layout {
background: #111;
color:#cecece;
.q-layout-header {
background: #111;
box-shadow: none;
border-bottom: 1px solid #333;
}
}
.q-layout-footer.status-footer {
background: #111;
color: #cecece;
border-top: 1px solid #333; border-top: 1px solid #333;
} }
}
.q-list-dark .q-item, .header-popover.q-popover {
.q-item-dark .q-item, background: $primary;
.q-list-header { color: white;
color: #cecece;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid $loki-green-dark-solid;
} }
}
.q-stepper-tab.step-waiting, .modal.minimized {
.q-stepper-step-content{ .q-input {
color: #cecece; .q-input-target {
color: #cecece;
}
&.q-if:hover:before {
color: #cecece;
}
} }
}
.modal {
.q-popover { .modal-content,
background: #222; .modal-body {
background: $loki-black-80;
color:#cecece; color:#cecece;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid #333;
}
} }
.modal.minimized { .q-layout-header {
.q-input {
.q-input-target { .q-toolbar {
color: #cecece; &>* {
}
&.q-if:hover:before {
color: #cecece; color: #cecece;
} }
} }
} }
.modal { }
.modal-content, .loki-green {
.modal-body { background: $loki-green;
background: #111; color: white;
color:#cecece; }
}
.startup-icons {
.q-layout-header { .solid {
background: #111; color:$loki-green-solid;
box-shadow: none; g,path {
border-bottom: 1px solid #333; fill: $loki-green-solid;
}
.q-toolbar { }
&>* { }
color: #cecece;
} .hr-separator {
} height: 2px;
} background: $secondary;
opacity: 0.4;
} }
.navigation {
.tx-list { .q-btn {
color: white;
.q-item.tx-in, background: $loki-black-90;
.q-item.tx-pool { }
.q-icon {
color: #cecece; .router-link-exact-active > .q-btn {
} background: $loki-green;
.q-item-label { }
color: lightgreen;
} }
&>div:last-child { .wallet-list {
text-align:right; .q-item-label {
&>div:first-child { color: white;
span { }
color: lightgreen;
&:before { .q-item {
content: "+"; background: $secondary;
color: lightgreen; .wallet-icon {
} color: $tertiary;
} g,path {
} fill: $tertiary;
} }
} }
}
.q-item.tx-out,
.q-item.tx-pending { .q-item:hover {
.q-icon { background: $primary !important;
color: #cecece;
} .wallet-icon {
.q-item-label { color:$loki-green-solid;
color: mediumpurple; g,path {
} fill: $loki-green-solid;
&>div:last-child { }
text-align:right; }
&>div:first-child {
span { .q-item-sublabel {
color: mediumpurple; color: white
&:before { }
content: "-"; }
color: mediumpurple; }
}
} .receive {
} .q-list-header {
} color: #FFFFFF;
} }
} .q-item-sublabel {
color: $loki-black-50;
.q-layout-footer.status-footer { }
.status-bars { .q-item-separator-component {
background-color: $secondary;
div:first-child { opacity: 0.4;
background-color: goldenrod; }
}
.item-group {
div:last-child { background: #313131;
background-color: #497dc6;
} -webkit-transition: background-color 0.2s ease-in;
transition: background-color 0.2s ease-in;
}
.info {
} span {
font-size: 14px;
}
.value {
font-size: 16px;
font-weight: bold;
}
}
}
.item-group:hover {
background: rgba(117,117,117,0.3);
}
.primary-address {
background: #3eb13e;
.q-item, .q-item-side {
color: white;
}
.q-item-sublabel {
color: rgba(255,255,255,0.9);
}
}
.primary-address:hover {
background: $loki-green-solid;
}
}
.tx-list {
.transaction {
background: #313131;
-webkit-transition: background-color 0.2s ease-in;
transition: background-color 0.2s ease-in;
.main {
border-left: 1px solid #252525;
}
}
.transaction:hover {
background: rgba(117,117,117,0.3);
}
}
.loki-field {
.content {
border: 1px solid #484848;
-webkit-transition: background-color 0.2s ease-in, border-color 0.2s ease-in;
transition: background-color 0.2s ease-in, border-color 0.2s ease-in;
}
&:not(.disable):not(.disable-hover) {
.content:hover {
background: #2e2e2e;
}
}
&.disable {
.content {
border-color: #404040 !important;
}
}
.content.error {
border-color: red;
}
.label {
color: white;
.optional {
color: #7C7C7C;
}
}
}
.service-node-page {
.address-type {
color: $loki-green-solid;
&.not-ours {
color: goldenrod;
}
}
} }

View file

@ -14,12 +14,21 @@
// It"s highly recommended to change the default colors // It"s highly recommended to change the default colors
// to match your app"s branding. // to match your app"s branding.
$primary = #027be3 $primary = $loki-green
$secondary = #26A69A $secondary = $loki-black-90
$tertiary = #555 $tertiary = $loki-black-80
$neutral = #E0E1E2 $neutral = #E0E1E2
$positive = #21BA45 $positive = #21BA45
$negative = #DB2828 $negative = #DB2828
$info = #31CCEC $info = #31CCEC
$warning = #F2C037 $warning = #F2C037
$loki-green = linear-gradient(180deg, #419B41 0%, #43BD43 100%)
$loki-green-solid = #5BCA5B;
$loki-green-dark-solid = #419B41;
$loki-black-90 = #0A0A0A
$loki-black-80 = #252525
$loki-black-60 = #313131
$loki-black-50 = #7E7E7E;

View file

@ -1,15 +1,17 @@
import { ipcRenderer } from "electron" import { ipcRenderer } from "electron"
import { Notify, Dialog, Loading, LocalStorage } from "quasar" import { Notify, Dialog, Loading, LocalStorage } from "quasar"
import { EventEmitter } from "events"
import { SCEE } from "./SCEE-Node" import { SCEE } from "./SCEE-Node"
export class Gateway { export class Gateway extends EventEmitter {
constructor (app, router) { constructor (app, router) {
super()
this.app = app this.app = app
this.router = router this.router = router
this.token = null this.token = null
this.scee = new SCEE() this.scee = new SCEE()
let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "light" let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "dark"
this.app.store.commit("gateway/set_app_data", { this.app.store.commit("gateway/set_app_data", {
config: { config: {
appearance: { appearance: {
@ -98,6 +100,9 @@ export class Gateway {
!decrypted_data.hasOwnProperty("data")) { return } !decrypted_data.hasOwnProperty("data")) { return }
switch (decrypted_data.event) { switch (decrypted_data.event) {
case "set_valid_address":
this.emit("validate_address", decrypted_data.data)
break
case "set_app_data": case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data) this.app.store.commit("gateway/set_app_data", decrypted_data.data)
break break
@ -111,10 +116,18 @@ export class Gateway {
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data) this.app.store.commit("gateway/set_wallet_data", decrypted_data.data)
break break
case "reset_wallet_error":
this.app.store.dispatch("gateway/resetWalletStatus")
break
case "set_tx_status": case "set_tx_status":
this.app.store.commit("gateway/set_tx_status", decrypted_data.data) this.app.store.commit("gateway/set_tx_status", decrypted_data.data)
break break
case "set_stake_status":
this.app.store.commit("gateway/set_stake_status", decrypted_data.data)
break
case "wallet_list": case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data) this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break break

View file

@ -13,7 +13,7 @@
<q-toolbar-title v-if="page_title=='Loki'"> <q-toolbar-title v-if="page_title=='Loki'">
<div style="margin-top:7px"> <div style="margin-top:7px">
<img src="statics/ryo-wallet.svg" height="32"> <img src="statics/loki.svg" height="32">
</div> </div>
</q-toolbar-title> </q-toolbar-title>
<q-toolbar-title v-else> <q-toolbar-title v-else>

View file

@ -3,31 +3,57 @@
<q-layout-header class="shift-title"> <q-layout-header class="shift-title">
<main-menu /> <main-menu />
<q-tabs class="col" align="justify" :color="theme == 'dark' ? 'light' : 'dark'" inverted>
<q-route-tab to="/wallet" default slot="title">
<span><q-icon name="attach_money" /> Wallet</span>
</q-route-tab>
<q-route-tab to="/wallet/receive" slot="title">
<span><q-icon name="call_received" /> Receive</span>
</q-route-tab>
<q-route-tab to="/wallet/send" slot="title">
<span><q-icon name="call_made" /> Send</span>
</q-route-tab>
<q-route-tab to="/wallet/addressbook" slot="title">
<span><q-icon name="person" /> Address Book</span>
</q-route-tab>
<q-route-tab to="/wallet/servicenode" slot="title">
<span><q-icon name="router" /> Service Node</span>
</q-route-tab>
<q-route-tab to="/wallet/txhistory" slot="title">
<span><q-icon name="history" /> TX History</span>
</q-route-tab>
</q-tabs>
</q-layout-header> </q-layout-header>
<q-page-container> <q-page-container>
<!-- <AddressHeader :address="info.address" :title="info.name" /> -->
<WalletDetails />
<div class="navigation row items-end">
<router-link to="/wallet">
<q-btn
class="single-icon"
size="md"
icon="swap_horiz"
/>
</router-link>
<router-link to="/wallet/send">
<q-btn
class="large-btn"
label="Send"
size="md"
icon-right="arrow_right_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/receive">
<q-btn
class="large-btn"
label="Receive"
size="md"
icon-right="save_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/servicenode">
<q-btn
class="large-btn"
label="Service node"
size="md"
icon-right="router"
align="left"
/>
</router-link>
<router-link to="/wallet/addressbook" class="address">
<q-btn
class="single-icon"
size="md"
icon="person"
/>
</router-link>
</div>
<div class="hr-separator" />
<keep-alive> <keep-alive>
<router-view /> <router-view />
</keep-alive> </keep-alive>
@ -39,14 +65,18 @@
</template> </template>
<script> <script>
const { clipboard } = require("electron")
import { openURL } from "quasar" import { openURL } from "quasar"
import { mapState } from "vuex" import { mapState } from "vuex"
import WalletDetails from "components/wallet_details"
import FormatLoki from "components/format_loki"
import StatusFooter from "components/footer" import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu" import MainMenu from "components/mainmenu"
export default { export default {
name: "LayoutDefault", name: "LayoutDefault",
computed: mapState({ computed: mapState({
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}), }),
data() { data() {
return { return {
@ -58,10 +88,40 @@ export default {
}, },
components: { components: {
StatusFooter, StatusFooter,
MainMenu MainMenu,
WalletDetails
} }
} }
</script> </script>
<style> <style lang="scss">
.navigation {
padding: 12px;
> * {
margin-right: 12px;
}
> *:last-child {
margin-right: 0px;
}
.address {
margin-left: auto;
}
.single-icon {
width: 38px;
padding: 0;
}
.large-btn {
width: 160px;
.q-btn-inner > *:last-child {
margin-left: auto;
}
}
}
</style> </style>

View file

@ -2,32 +2,7 @@
<q-page> <q-page>
<div class="init-screen-page text-center"> <div class="init-screen-page text-center">
<div class="absolute-center"> <div class="absolute-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379"> <img src="statics/loki.svg" width="400" class="q-mb-md">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<div class="startup-icons q-mt-xl q-mb-lg"> <div class="startup-icons q-mt-xl q-mb-lg">
<div ref="backend"> <div ref="backend">
@ -149,50 +124,21 @@ export default {
&>div { &>div {
display: inline-block; display: inline-block;
margin: 0 15px; margin: 0 15px;
color: lightgrey; color: #444;
g,path { g,path {
fill: lightgrey; fill: #444;
}
}
.solid {
color: var(--q-color-primary);
g,path {
fill: var(--q-color-primary);
} }
} }
.pulse { .pulse {
color:black; color:#cecece;
opacity: 0.6; opacity: 0.6;
animation: fade 2s infinite; animation: fade 2s infinite;
g,path { g,path {
fill:black; fill:#cecece;
}
}
}
.dark {
.startup-icons {
&>div {
display: inline-block;
margin: 0 15px;
color: #444;
g,path {
fill: #444;
}
}
.solid {
color: var(--q-color-primary);
g,path {
fill: var(--q-color-primary);
}
}
.pulse {
color: #cecece;
g,path {
fill: #cecece;
}
} }
} }
} }
@keyframes fade { @keyframes fade {
0%,100% { opacity: 0.3 } 0%,100% { opacity: 0.3 }
50% { opacity: 0.6 } 50% { opacity: 0.6 }

View file

@ -2,32 +2,7 @@
<q-page> <q-page>
<div class="init-screen-page text-center"> <div class="init-screen-page text-center">
<div class="absolute-center"> <div class="absolute-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379"> <img src="statics/loki.svg" width="400" class="q-mb-md">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<div class="q-mt-xl q-mb-lg"> <div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" /> <q-spinner color="primary" :size="30" />

View file

@ -1,56 +1,27 @@
<template> <template>
<q-page> <q-page class="welcome">
<q-stepper class="no-shadow" ref="stepper" :color="theme == 'dark' ? 'light' : 'dark'" dark> <q-stepper class="no-shadow" ref="stepper" :color="theme == 'dark' ? 'light' : 'dark'" dark>
<q-step default title="Welcome"> <q-step default title="Welcome" class="first-step">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 859.4011 116.8585" height="64"> <div class="welcome-container">
<defs> <img src="statics/loki.svg" height="100" class="q-mb-md">
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse"> <div>Version: ATOM v{{version}}-v{{daemonVersion}}</div>
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z"/>
</g>
</svg>
<div>Version: ATOM v{{version}}-v{{daemonVersion}}</div> <h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<h6 class="q-mb-md" style="font-weight: 300">Select Appearance:</h6> <q-btn-toggle
v-model="choose_lang"
toggle-color="primary"
size="md"
:options="[
{label: 'English', value: 'EN', icon: 'language'},
]"
/>
<q-btn-toggle <p class="q-mt-md">More languages coming soon</p>
v-model="choose_theme" </div>
toggle-color="primary"
size="md"
:options="[
{label: 'Light theme', value: 'light', icon: 'brightness_5'},
{label: 'Dark theme', value: 'dark', icon: 'brightness_2'},
]"
/>
<h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<q-btn-toggle
v-model="choose_lang"
toggle-color="primary"
size="md"
:options="[
{label: 'English', value: 'EN', icon: 'language'},
]"
/>
<p class="q-mt-md">More languages coming soon</p>
</q-step> </q-step>
@ -59,37 +30,6 @@
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral> <SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</q-step> </q-step>
<q-step title="Review">
<h2 class="q-mt-none q-mb-none text-weight-thin">You're almost set!</h2>
<h6 class="q-mb-md q-mt-md" style="font-weight: 300">Review settings:</h6>
<p>You are using a
<template v-if="config_daemon.type == 'local'">
<code>local node</code>
</template>
<template v-if="config_daemon.type == 'local_remote'">
<code>local + remote node</code>
</template>
<template v-if="config_daemon.type == 'remote'">
<code>remote node</code>
</template>
<template v-if="pending_config.app.net_type == 'test'">
<code>on testnet</code>
</template>
<template v-if="pending_config.app.net_type == 'staging'">
<code>on staging</code>
</template>
and will store data in
<code>{{ pending_config.app.data_dir }}</code>
</p>
<p>Press next to get started!</p>
</q-step>
</q-stepper> </q-stepper>
<q-layout-footer class="no-shadow q-pa-sm"> <q-layout-footer class="no-shadow q-pa-sm">
@ -130,23 +70,11 @@ export default {
}), }),
data() { data() {
return { return {
choose_theme: "light",
choose_lang: "EN", choose_lang: "EN",
version: "", version: "",
daemonVersion: "" daemonVersion: ""
} }
}, },
watch: {
choose_theme: function (val) {
this.$store.commit("gateway/set_app_data", {
config: {
appearance: {
theme: val
}
}
});
}
},
mounted () { mounted () {
this.version = version this.version = version
@ -180,6 +108,21 @@ export default {
<style lang="scss"> <style lang="scss">
.welcome {
.welcome-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.first-step .q-stepper-step-inner {
min-height: 250px;
height: calc(100vh - 102px);
}
}
.language-item { .language-item {
padding: 10px 30px 10px 20px; padding: 10px 30px 10px 20px;

View file

@ -1,32 +1,43 @@
<template> <template>
<q-page> <q-page class="create-wallet">
<div class="q-mx-md"> <div class="fields q-mx-md q-mt-md">
<q-field class="q-mt-none"> <LokiField label="Wallet name" :error="$v.wallet.name.$error">
<q-input <q-input
v-model="wallet.name" v-model="wallet.name"
float-label="Wallet name"
@blur="$v.wallet.name.$touch" @blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'" :dark="theme=='dark'"
/> placeholder="A name for your wallet"
</q-field> hide-underline
/>
</LokiField>
<q-field> <LokiField label="Seed Language">
<q-select <q-select
v-model="wallet.language" v-model="wallet.language"
float-label="Seed language"
:options="languageOptions" :options="languageOptions"
:dark="theme=='dark'" :dark="theme=='dark'"
/> hide-underline
</q-field> />
</LokiField>
<q-field> <LokiField label="Password" optional>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" /> <q-input
</q-field> v-model="wallet.password"
type="password"
:dark="theme=='dark'"
placeholder="An optional password for the wallet"
hide-underline
/>
</LokiField>
<q-field> <LokiField label="Confirm Password">
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" /> <q-input
</q-field> v-model="wallet.password_confirm"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field> <q-field>
<q-btn color="primary" @click="create" label="Create wallet" /> <q-btn color="primary" @click="create" label="Create wallet" />
@ -39,6 +50,7 @@
<script> <script>
import { required } from "vuelidate/lib/validators" import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex" import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default { export default {
data () { data () {
return { return {
@ -130,9 +142,19 @@ export default {
cancel() { cancel() {
this.$router.replace({ path: "/wallet-select" }); this.$router.replace({ path: "/wallet-select" });
} }
},
components: {
LokiField
} }
} }
</script> </script>
<style> <style lang="scss">
.create-wallet {
.fields {
> * {
margin-bottom: 16px;
}
}
}
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<q-page padding> <q-page padding>
<AddressHeader :address="info.address" :title="info.name" /> <AddressHeader :address="info.address" :title="walletName"/>
<template v-if="secret.mnemonic"> <template v-if="secret.mnemonic">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6> <h6 class="q-mb-xs q-mt-lg">Seed words</h6>
@ -75,6 +75,9 @@ export default {
computed: mapState({ computed: mapState({
info: state => state.gateway.wallet.info, info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret, secret: state => state.gateway.wallet.secret,
walletName (state) {
return `Your Wallet (${this.info.name})`
}
}), }),
methods: { methods: {
open() { open() {

View file

@ -137,7 +137,18 @@ export default {
validations: { validations: {
wallet: { wallet: {
name: { required }, name: { required },
address: { required, address }, address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
viewkey: { required, privkey }, viewkey: { required, privkey },
refresh_start_height: { numeric } refresh_start_height: { numeric }
} }

View file

@ -1,36 +1,30 @@
<template> <template>
<q-page> <q-page>
<div class="q-mx-md"> <div class="q-mx-md import-wallet">
<q-field class="q-mt-none"> <LokiField label="New wallet name" :error="$v.wallet.name.$error">
<q-input <q-input
v-model="wallet.name" v-model="wallet.name"
float-label="New wallet name" placeholder="A name for your wallet"
@blur="$v.wallet.name.$touch" @blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'" :dark="theme=='dark'"
hide-underline
/> />
</q-field> </LokiField>
<q-field> <LokiField label="Wallet file" disable-hover>
<div class="row gutter-sm"> <q-input v-model="wallet.path" placeholder="Please select a file" disable :dark="theme=='dark'" hide-underline/>
<div class="col"> <input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden />
<q-input v-model="wallet.path" stack-label="Wallet file" disable :dark="theme=='dark'" /> <q-btn color="secondary" v-on:click="selectFile" :text-color="theme=='dark'?'white':'dark'">Select wallet file</q-btn>
<input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden /> </LokiField>
</div>
<div class="col-auto">
<q-btn v-on:click="selectFile" class="float-right" :text-color="theme=='dark'?'white':'dark'">Select wallet file</q-btn>
</div>
</div>
</q-field>
<q-field> <LokiField label="Password">
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" /> <q-input v-model="wallet.password" placeholder="An optional password for the wallet" type="password" :dark="theme=='dark'" hide-underline />
</q-field> </LokiField>
<q-field> <LokiField label="Confirm Password">
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" /> <q-input v-model="wallet.password_confirm" type="password" :dark="theme=='dark'" hide-underline />
</q-field> </LokiField>
<q-field> <q-field>
<q-btn color="primary" @click="import_wallet" label="Import wallet" /> <q-btn color="primary" @click="import_wallet" label="Import wallet" />
@ -44,6 +38,7 @@
<script> <script>
import { required } from "vuelidate/lib/validators" import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex" import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default { export default {
data () { data () {
return { return {
@ -124,9 +119,24 @@ export default {
cancel() { cancel() {
this.$router.replace({ path: "/wallet-select" }); this.$router.replace({ path: "/wallet-select" });
} }
},
components: {
LokiField
} }
} }
</script> </script>
<style> <style lang="scss">
.import-wallet {
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
}
.loki-field {
margin-top: 16px;
}
}
</style> </style>

View file

@ -1,27 +1,31 @@
<template> <template>
<q-page> <q-page>
<q-list class="wallet-list" link no-border :dark="theme=='dark'">
<q-list link no-border :dark="theme=='dark'">
<template v-if="wallets.list.length"> <template v-if="wallets.list.length">
<q-list-header>Open wallet</q-list-header> <div class="header row justify-between items-center">
<q-item v-for="(wallet, index) in wallets.list" @click.native="openWallet(wallet)"> <div class="header-title">Your wallets</div>
<q-btn class="add" icon="add" size="md" color="primary" v-if="wallets.list.length">
<q-popover class="header-popover">
<q-list separator link>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</q-list>
</q-popover>
</q-btn>
</div>
<div class="hr-separator" />
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)" :key="wallet.address">
<q-item-side> <q-item-side>
<Identicon :address="wallet.address" :ref="`${index}-identicon`" /> <div class="wallet-icon">
<svg width="48" viewBox="0 0 17 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-wallet"><title>969</title><defs class="si-glyph-fill"></defs><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 0.000000)" fill="#434343"><path d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z" class="si-glyph-fill"></path><path d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z" class="si-glyph-fill"></path><path d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z" class="si-glyph-fill"></path></g></g></svg>
</div>
</q-item-side> </q-item-side>
<q-item-main> <q-item-main>
<q-item-tile label>{{ wallet.name }}</q-item-tile> <q-item-tile label>{{ wallet.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile> <q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile>
</q-item-main> </q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(wallet.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-context-menu> <q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;"> <q-list link separator style="min-width: 150px; max-height: 300px;">
@ -34,33 +38,15 @@
@click.native="copyAddress(wallet.address, $event)"> @click.native="copyAddress(wallet.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-identicon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
</q-item> </q-item>
<q-item-separator /> <q-item-separator />
</template> </template>
<q-item @click.native="createNewWallet()"> <template v-else>
<q-item-main label="Create new wallet" /> <q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
</q-item> <q-item-main :label="action.name" />
<q-item @click.native="restoreWallet()">
<q-item-main label="Restore wallet from seed (Currently not working)" />
</q-item>
<!-- TODO: Re-enable this when LOKI has the functionality -->
<!-- <q-item @click.native="restoreViewWallet()">
<q-item-main label="Restore view-only wallet" />
</q-item> -->
<q-item @click.native="importWallet()">
<q-item-main label="Import wallet from file" />
</q-item>
<template v-if="wallets.legacy.length">
<q-item @click.native="importLegacyWallet()">
<q-item-main label="Import wallet from legacy gui" />
</q-item> </q-item>
</template> </template>
</q-list> </q-list>
@ -76,7 +62,27 @@ export default {
computed: mapState({ computed: mapState({
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
wallets: state => state.gateway.wallets, wallets: state => state.gateway.wallets,
status: state => state.gateway.wallet.status status: state => state.gateway.wallet.status,
actions (status) {
// TODO: Add this in once LOKI has the functionality
// <q-item @click.native="restoreViewWallet()">
// <q-item-main label="Restore view-only wallet" />
// </q-item>
return [
{
name: "Create new wallet",
handler: this.createNewWallet,
},
{
name: "Restore wallet from seed",
handler: this.restoreWallet,
},
{
name: "Import wallet from file",
handler: this.importWallet,
}
];
}
}), }),
methods: { methods: {
openWallet(wallet) { openWallet(wallet) {
@ -176,5 +182,28 @@ export default {
} }
</script> </script>
<style> <style lang="scss">
.wallet-list {
.header {
margin: 0 16px;
margin-bottom: 8px;
min-height: 36px;
.header-title {
font-size: 14px;
font-weight: 500;
}
.add {
width: 38px;
padding: 0;
}
}
.q-item {
margin: 10px 16px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
}
</style> </style>

View file

@ -1,74 +1,83 @@
<template> <template>
<q-page> <q-page>
<div class="q-mx-md"> <div class="q-mx-md">
<q-field class="q-mt-none"> <LokiField class="q-mt-md" label="Wallet name" :error="$v.wallet.name.$error">
<q-input <q-input
v-model="wallet.name" v-model="wallet.name"
float-label="Wallet name" placeholder="A name for your wallet"
@blur="$v.wallet.name.$touch" @blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'" :dark="theme=='dark'"
hide-underline
/> />
</q-field> </LokiField>
<q-field> <LokiField class="q-mt-md" label="Mnemonic seed" :error="$v.wallet.seed.$error">
<q-input <q-input
v-model="wallet.seed" v-model="wallet.seed"
float-label="Mnemonic seed" placeholder="25 (or 24) word mnemonic seed"
type="textarea" type="textarea"
@blur="$v.wallet.seed.$touch" @blur="$v.wallet.seed.$touch"
:error="$v.wallet.seed.$error"
:dark="theme=='dark'" :dark="theme=='dark'"
hide-underline
/> />
</q-field> </LokiField>
<q-field> <div class="row items-end q-mt-md">
<div class="row items-center gutter-sm"> <div class="col">
<div class="col"> <LokiField v-if="wallet.refresh_type=='date'" label="Restore from date">
<template v-if="wallet.refresh_type=='date'"> <q-datetime v-model="wallet.refresh_start_date" type="date"
<q-datetime v-model="wallet.refresh_start_date" type="date" modal :min="1492486495000" :max="Date.now()"
float-label="Restore from date" :dark="theme=='dark'"
modal :min="1492486495000" :max="Date.now()" hide-underline
:dark="theme=='dark'" />
/> </LokiField>
</template> <LokiField v-else-if="wallet.refresh_type=='height'" label="Restore from block height" :error="$v.wallet.refresh_start_height.$error">
<template v-else-if="wallet.refresh_type=='height'"> <q-input v-model="wallet.refresh_start_height" type="number"
<q-input v-model="wallet.refresh_start_height" type="number" min="0"
min="0" float-label="Restore from block height" @blur="$v.wallet.refresh_start_height.$touch"
@blur="$v.wallet.refresh_start_height.$touch" :dark="theme=='dark'"
:error="$v.wallet.refresh_start_height.$error" hide-underline
:dark="theme=='dark'" />
/> </LokiField>
</template>
</div>
<div class="col-auto">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br/>height select</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br/>date select</div>
</div>
</q-btn>
</template>
</div>
</div> </div>
</q-field> <div class="col-auto q-ml-sm">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br/>height select</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br/>date select</div>
</div>
</q-btn>
</template>
</div>
</div>
<q-field> <LokiField class="q-mt-md" label="Password">
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" /> <q-input
</q-field> v-model="wallet.password"
placeholder="An optional password for the wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field> <LokiField class="q-mt-md" label="Confirm Password">
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" /> <q-input
</q-field> v-model="wallet.password_confirm"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field> <q-field>
<q-btn color="primary" @click="restore_wallet" label="Restore wallet" /> <q-btn color="primary" @click="restore_wallet" label="Restore wallet" />
@ -81,6 +90,7 @@
<script> <script>
import { required, numeric } from "vuelidate/lib/validators" import { required, numeric } from "vuelidate/lib/validators"
import { mapState } from "vuex" import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default { export default {
data () { data () {
return { return {
@ -187,6 +197,9 @@ export default {
cancel() { cancel() {
this.$router.replace({ path: "/wallet-select" }); this.$router.replace({ path: "/wallet-select" });
} }
},
components: {
LokiField
} }
} }
</script> </script>

View file

@ -1,39 +1,43 @@
<template> <template>
<q-page> <q-page class="receive">
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_received" size="24px" /> Receive Loki
</div>
<div class="col-4">
</div>
</div>
<q-list link no-border :dark="theme=='dark'"> <q-list link no-border :dark="theme=='dark'">
<q-list-header>My primary address</q-list-header> <q-list-header>My primary address</q-list-header>
<q-item v-for="(address, index) in address_list.primary" @click.native="details(address)"> <q-list class="item-group primary-address" no-border v-for="address in address_list.primary" :key="address.address" @click.native="details(address)">
<q-item-side> <q-item>
<Identicon :address="address.address" ref="primaryIdenticon" /> <q-item-main>
</q-item-side> <q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-main> <q-item-tile sublabel>Primary address</q-item-tile>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile> </q-item-main>
<q-item-tile sublabel>Primary address</q-item-tile> <q-item-side>
</q-item-main> <q-btn
<q-item-side> flat
<q-btn style="width:25px;"
color="primary" style="width:25px;" size="md" icon="file_copy"
size="sm" icon="file_copy" @click="copyAddress(address.address, $event)">
@click="copyAddress(address.address, $event)"> <q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]"> Copy address
Copy address </q-tooltip>
</q-tooltip> </q-btn>
</q-btn> </q-item-side>
</q-item-side> </q-item>
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>Balance</span>
<span class="value">{{address.balance | currency}}</span>
</div>
<div class="column">
<span>Unlocked balance</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>Unspent outputs</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
<q-context-menu> <q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;"> <q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay <q-item v-close-overlay
@ -45,36 +49,47 @@
@click.native="copyAddress(address.address, $event)"> @click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs.primaryIdenticon[0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
</q-list>
</q-item>
<template v-if="address_list.used.length"> <template v-if="address_list.used.length">
<q-list-header>My used addresses</q-list-header> <q-list-header>My used addresses</q-list-header>
<q-item v-for="(address, index) in address_list.used" @click.native="details(address)"> <q-list class="item-group" no-border v-for="address in address_list.used" @click.native="details(address)" :key="address.address">
<q-item-side> <q-item>
<Identicon :address="address.address" :ref="`${index}-usedIdenticon`" /> <q-item-main>
</q-item-side> <q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-main> <q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile> </q-item-main>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile> <q-item-side>
</q-item-main> <q-btn
<q-item-side> flat
<q-btn style="width:25px;"
color="primary" style="width:25px;" size="md" icon="file_copy"
size="sm" icon="file_copy" @click="copyAddress(address.address, $event)">
@click="copyAddress(address.address, $event)"> <q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]"> Copy address
Copy address </q-tooltip>
</q-tooltip> </q-btn>
</q-btn> </q-item-side>
</q-item-side> </q-item>
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>Balance</span>
<span class="value">{{ address.balance | currency }}</span>
</div>
<div class="column">
<span>Unlocked balance</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>Unspent outputs</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
<q-context-menu> <q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;"> <q-list link separator style="min-width: 150px; max-height: 300px;">
@ -87,38 +102,32 @@
@click.native="copyAddress(address.address, $event)"> @click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-usedIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
</q-list>
</q-item>
</template> </template>
<template v-if="address_list.unused.length"> <template v-if="address_list.unused.length">
<q-list-header>My unused addresses</q-list-header> <q-list-header>My unused addresses</q-list-header>
<q-item v-for="(address, index) in address_list.unused" @click.native="details(address)"> <q-list class="item-group" no-border v-for="address in address_list.unused" @click.native="details(address)" :key="address.address">
<q-item-side> <q-item>
<Identicon :address="address.address" :ref="`${index}-unusedIdenticon`" /> <q-item-main>
</q-item-side> <q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-main> <q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile> </q-item-main>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile> <q-item-side>
</q-item-main> <q-btn
<q-item-side> flat
<q-btn style="width:25px;"
color="primary" style="width:25px;" size="md" icon="file_copy"
size="sm" icon="file_copy" @click="copyAddress(address.address, $event)">
@click="copyAddress(address.address, $event)"> <q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]"> Copy address
Copy address </q-tooltip>
</q-tooltip> </q-btn>
</q-btn> </q-item-side>
</q-item-side> </q-item>
<q-context-menu> <q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;"> <q-list link separator style="min-width: 150px; max-height: 300px;">
@ -131,15 +140,10 @@
@click.native="copyAddress(address.address, $event)"> @click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" /> <q-item-main label="Copy address" />
</q-item> </q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-unusedIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list> </q-list>
</q-context-menu> </q-context-menu>
</q-item> </q-list>
</template> </template>
</q-list> </q-list>
@ -147,10 +151,8 @@
</q-page> </q-page>
</template> </template>
<style>
</style>
<script> <script>
const { clipboard } = require("electron") const { clipboard } = require("electron")
import { mapState } from "vuex" import { mapState } from "vuex"
import Identicon from "components/identicon" import Identicon from "components/identicon"
@ -160,6 +162,18 @@ export default {
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
address_list: state => state.gateway.wallet.address_list address_list: state => state.gateway.wallet.address_list
}), }),
filters: {
toString: function (value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
currency: function (value) {
if (typeof value !== "number") return "N/A";
const amount = value / 1e9
return amount.toLocaleString()
}
},
methods: { methods: {
details (address) { details (address) {
this.$refs.addressDetails.address = address; this.$refs.addressDetails.address = address;
@ -187,3 +201,33 @@ export default {
} }
} }
</script> </script>
<style lang="scss">
.receive {
.q-item-label {
font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
}
.item-group {
cursor: pointer;
margin: 0 16px;
// padding: 14px;
border-radius: 3px;
+ .item-group {
margin-top: 10px;
}
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
}
}
}
</style>

View file

@ -1,103 +1,97 @@
<template> <template>
<q-page> <q-page class="send">
<template v-if="view_only"> <template v-if="view_only">
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_made" size="24px" /> Send Loki
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md"> <div class="q-pa-md">
View-only mode. Please load full wallet in order to send coins. View-only mode. Please load full wallet in order to send coins.
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_made" size="24px" /> Send Loki
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md"> <div class="q-pa-md">
<div class="row gutter-md">
<div class="row items-end gutter-md"> <!-- Amount -->
<div class="col-6">
<div class="col"> <LokiField label="Amount" :error="$v.newTx.amount.$error">
<q-field class="q-ma-none"> <q-input v-model="newTx.amount"
<q-input v-model="newTx.amount" float-label="Amount" :dark="theme=='dark'" :dark="theme=='dark'"
type="number" min="0" :max="unlocked_balance / 1e9" /> type="number"
</q-field> min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.newTx.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All</q-btn>
</LokiField>
</div> </div>
<div> <!-- Priority -->
<q-btn @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All coins</q-btn> <div class="col-6">
<LokiField label="Priority">
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
:options="priorityOptions"
hide-underline
/>
</LokiField>
</div> </div>
</div> </div>
<q-item class="q-pa-none"> <!-- Address -->
<q-item-side> <div class="col q-mt-sm">
<Identicon :address="newTx.address" menu /> <LokiField label="Address" :error="$v.newTx.address.$error">
</q-item-side> <q-input v-model="newTx.address"
<q-item-main> :dark="theme=='dark'"
<q-field> @blur="$v.newTx.address.$touch"
<q-input v-model="newTx.address" float-label="Address" :placeholder="address_placeholder"
:dark="theme=='dark'" hide-underline
@blur="$v.newTx.address.$touch" />
:error="$v.newTx.address.$error" <q-btn color="secondary" :text-color="theme=='dark'?'white':'dark'" to="addressbook">Contacts</q-btn>
/> </LokiField>
</q-field> </div>
</q-item-main>
</q-item>
<q-field style="margin-top:0">
<q-input v-model="newTx.payment_id" float-label="Payment ID (optional)"
:dark="theme=='dark'"
@blur="$v.newTx.payment_id.$touch"
:error="$v.newTx.payment_id.$error"
/>
</q-field>
<q-field>
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
float-label="Priority"
:options="priorityOptions"
/>
</q-field>
<!-- Payment ID -->
<div class="col q-mt-sm">
<LokiField label="Payment id" :error="$v.newTx.payment_id.$error" optional>
<q-input v-model="newTx.payment_id"
:dark="theme=='dark'"
@blur="$v.newTx.payment_id.$touch"
placeholder="16 or 64 hexadecimal characters"
hide-underline
/>
</LokiField>
</div>
<!-- Save to address book -->
<q-field> <q-field>
<q-checkbox v-model="newTx.address_book.save" label="Save to address book" :dark="theme=='dark'" /> <q-checkbox v-model="newTx.address_book.save" label="Save to address book" :dark="theme=='dark'" />
</q-field> </q-field>
<div v-if="newTx.address_book.save"> <div v-if="newTx.address_book.save">
<q-field> <LokiField label="Name" optional>
<q-input v-model="newTx.address_book.name" float-label="Name" :dark="theme=='dark'" /> <q-input v-model="newTx.address_book.name"
</q-field> :dark="theme=='dark'"
<q-field> placeholder="Name that belongs to this address"
<q-input v-model="newTx.address_book.description" type="textarea" rows="2" float-label="Notes" :dark="theme=='dark'" /> hide-underline
</q-field> />
</LokiField>
<LokiField class="q-mt-sm" label="Notes" optional>
<q-input v-model="newTx.address_book.description"
type="textarea"
rows="2"
:dark="theme=='dark'"
placeholder="Additional notes"
hide-underline
/>
</LokiField>
</div> </div>
<q-field class="q-pt-sm"> <q-field class="q-pt-sm">
<q-btn <q-btn
class="send-btn"
:disable="!is_able_to_send" :disable="!is_able_to_send"
color="primary" @click="send()" label="Send" /> color="primary" @click="send()" label="Send" />
</q-field> </q-field>
@ -116,8 +110,9 @@
<script> <script>
import { mapState } from "vuex" import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators" import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, address } from "src/validators/common" import { payment_id, address, greater_than_zero } from "src/validators/common"
import Identicon from "components/identicon" import Identicon from "components/identicon"
import LokiField from "components/loki_field"
const objectAssignDeep = require("object-assign-deep"); const objectAssignDeep = require("object-assign-deep");
export default { export default {
computed: mapState({ computed: mapState({
@ -130,6 +125,11 @@ export default {
}, },
is_able_to_send (state) { is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"] return this.$store.getters["gateway/isAbleToSend"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
} }
}), }),
data () { data () {
@ -158,9 +158,21 @@ export default {
newTx: { newTx: {
amount: { amount: {
required, required,
decimal decimal,
greater_than_zero
},
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
}, },
address: { required, address },
payment_id: { payment_id } payment_id: { payment_id }
} }
}, },
@ -291,17 +303,23 @@ export default {
message: "Sending transaction", message: "Sending transaction",
sending: true sending: true
}) })
let newTx = objectAssignDeep.noMutate(this.newTx, {password}) const newTx = objectAssignDeep.noMutate(this.newTx, {password})
this.$gateway.send("wallet", "transfer", newTx) this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => { }).catch(() => {
}) })
} }
}, },
components: { components: {
Identicon Identicon,
LokiField
} }
} }
</script> </script>
<style> <style lang="scss">
.send {
.send-btn {
width: 200px;
}
}
</style> </style>

View file

@ -1,59 +1,44 @@
<template> <template>
<q-page class="service-node-page"> <q-page class="service-node-page">
<template> <template>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="router" size="24px" /> Service Nodes
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md"> <div class="q-pa-md">
<q-field> <LokiField label="Service Node Key" :error="$v.service_node.key.$error">
<q-input v-model="serviceNode.key" float-label="Service Node Key" <q-input v-model="service_node.key"
:dark="theme=='dark'" :dark="theme=='dark'"
@blur="$v.serviceNode.key.$touch" @blur="$v.service_node.key.$touch"
:error="$v.serviceNode.key.$error" placeholder="64 hexadecimal characters"
/> hide-underline
</q-field> />
</LokiField>
<q-item>
<q-item-main>
<q-item-tile label class="recepient-address">Award Recepient's Address (yours)</q-item-tile>
<q-item-tile class="monospace break-all" label>{{ info.address }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(info.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<div class="row items-end gutter-md">
<div class="col">
<q-field class="q-ma-none">
<q-input v-model="serviceNode.amount" float-label="Amount" :dark="theme=='dark'"
type="number" min="0" :max="unlocked_balance / 1e9" />
</q-field>
</div>
<div>
<q-btn @click="serviceNode.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All coins</q-btn>
</div>
<div class="q-mt-md col">
<LokiField label="Award Recepient's Address" :error="$v.service_node.award_address.$error">
<q-input v-model="service_node.award_address"
:dark="theme=='dark'"
@blur="$v.service_node.award_address.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<div class="address-type" :class="[addressType]">( {{ addressType | addressTypeString }} )</div>
</div> </div>
<LokiField label="Amount" class="q-mt-md" :error="$v.service_node.amount.$error">
<q-input v-model="service_node.amount"
:dark="theme=='dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.service_node.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="service_node.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All</q-btn>
</LokiField>
<q-field class="q-pt-sm"> <q-field class="q-pt-sm">
<q-btn <q-btn
:disable="!is_able_to_send" :disable="!is_able_to_send"
@ -62,9 +47,9 @@
</div> </div>
<!-- <q-inner-loading :visible="tx_status.sending" :dark="theme=='dark'"> <q-inner-loading :visible="stake_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" /> <q-spinner color="primary" :size="30" />
</q-inner-loading> --> </q-inner-loading>
</template> </template>
@ -75,81 +60,172 @@
const { clipboard } = require("electron") const { clipboard } = require("electron")
import { mapState } from "vuex" import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators" import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, service_node_key } from "src/validators/common" import { payment_id, service_node_key, greater_than_zero, address } from "src/validators/common"
import Identicon from "components/identicon" import Identicon from "components/identicon"
import LokiField from "components/loki_field"
const objectAssignDeep = require("object-assign-deep"); const objectAssignDeep = require("object-assign-deep");
export default { export default {
computed: mapState({ computed: mapState({
theme: state => state.gateway.app.config.appearance.theme, theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance, unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info, info: state => state.gateway.wallet.info,
address_list: state => state.gateway.wallet.address_list,
stake_status: state => state.gateway.stake_status,
is_ready (state) { is_ready (state) {
return this.$store.getters["gateway/isReady"] return this.$store.getters["gateway/isReady"]
}, },
is_able_to_send (state) { is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"] return this.$store.getters["gateway/isAbleToSend"]
},
addressType (state) {
const address = this.service_node.award_address;
const inArray = (array) => array.map(o => o.address).includes(address);
const { primary, used, unused } = this.address_list
if (inArray(primary)) {
return "primary"
} else if (inArray(used)) {
return "used"
} else if (inArray(unused)) {
return "unsued"
} else {
return "not-ours"
}
} }
}), }),
data () { data () {
return { return {
staking: false, service_node: {
serviceNode: {
key: "", key: "",
amount: 0, amount: 0,
award_address: "",
}, },
} }
}, },
filters: {
addressTypeString: function (value) {
switch (value) {
case "primary":
return "Your primary address"
case "used":
return "Your used address"
case "ununsed":
return "Your unused address"
default:
return "Not your address!"
}
}
},
validations: { validations: {
serviceNode: { service_node: {
key: { required, service_node_key },
amount: { amount: {
required, required,
decimal decimal,
greater_than_zero,
}, },
key: { required, service_node_key }, award_address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
}
}
},
watch: {
stake_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.stake_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.stake_status.message
})
this.$v.$reset();
this.service_node = {
key: "",
amount: 0,
award_address: "",
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.stake_status.message
})
break;
}
},
deep: true
},
},
created () {
const { address } = this.info;
if (!this.service_node.award_address || this.service_node.award_address === "") {
this.service_node.award_address = address || ""
} }
}, },
methods: { methods: {
copyAddress (address, event) { isOurAddress (address) {
event.stopPropagation() const { primary, used, unused } = this.address_list
for(let i = 0; i < event.path.length; i++) { const addresses = [...primary, ...used, ...unused].map(o => o.address);
if(event.path[i].tagName == "BUTTON") { console.log(addresses);
event.path[i].blur() return addresses.includes(address);
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
}, },
stake: function () { stake: function () {
this.$v.serviceNode.$touch() this.$v.service_node.$touch()
if(this.serviceNode.amount < 0) { if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
if (this.$v.service_node.award_address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Address not valid"
})
return
}
if(this.service_node.amount < 0) {
this.$q.notify({ this.$q.notify({
type: "negative", type: "negative",
timeout: 1000, timeout: 1000,
message: "Amount cannot be negative" message: "Amount cannot be negative"
}) })
return return
} else if(this.serviceNode.amount == 0) { } else if(this.service_node.amount == 0) {
this.$q.notify({ this.$q.notify({
type: "negative", type: "negative",
timeout: 1000, timeout: 1000,
message: "Amount must be greater than zero" message: "Amount must be greater than zero"
}) })
return return
} else if(this.serviceNode.amount > this.unlocked_balance / 1e9) { } else if(this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({ this.$q.notify({
type: "negative", type: "negative",
timeout: 1000, timeout: 1000,
message: "Not enough unlocked balance" message: "Not enough unlocked balance"
}) })
return return
} else if (this.$v.serviceNode.amount.$error) { } else if (this.$v.service_node.amount.$error) {
this.$q.notify({ this.$q.notify({
type: "negative", type: "negative",
timeout: 1000, timeout: 1000,
@ -158,15 +234,6 @@ export default {
return return
} }
if (this.$v.serviceNode.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
this.$q.dialog({ this.$q.dialog({
title: "Stake", title: "Stake",
message: "Enter wallet password to continue.", message: "Enter wallet password to continue.",
@ -183,34 +250,37 @@ export default {
color: this.theme=="dark"?"white":"dark" color: this.theme=="dark"?"white":"dark"
} }
}).then(password => { }).then(password => {
// this.$store.commit("gateway/set_tx_status", { this.$store.commit("gateway/set_stake_status", {
// code: 1, code: 1,
// message: "Sending transaction", message: "Staking...",
// sending: true sending: true
// }) })
// let newTx = objectAssignDeep.noMutate(this.newTx, {password}) const service_node = objectAssignDeep.noMutate(this.service_node, {password})
// this.$gateway.send("wallet", "transfer", newTx) this.$gateway.send("wallet", "stake", {
...service_node,
destination: service_node.award_address,
})
}).catch(() => { }).catch(() => {
}) })
} }
}, },
components: { components: {
Identicon, Identicon,
LokiField
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.service-node-page { .service-node-page {
.address-type {
.q-item { margin-top: 4px;
padding-left: 0; font-size: 13px;
padding-right: 0; font-weight: 400;
} text-align: right;
&.not-ours {
.recepient-address { font-weight: bold;
margin-bottom: 8px; }
font-size: 1rem;
} }
} }
</style> </style>

View file

@ -1,26 +1,27 @@
<template> <template>
<q-page> <q-page>
<div class="row q-pt-sm q-mx-md q-mb-sm items-center non-selectable"> <div class="row q-pt-sm q-mx-md q-mb-sm items-end non-selectable">
<div class="col-5"> <div class="col-5">
<q-icon name="history" size="24px" /> Transaction history Transactions
</div> </div>
<div class="col-5 q-px-sm"> <LokiField class="col-5 q-px-sm" label="Filter by txid">
<q-input v-model="tx_txid" <q-input v-model="tx_txid"
stack-label="Filter by txid"
:dark="theme=='dark'" :dark="theme=='dark'"
placeholder="Enter an ID"
hide-underline
/> />
</div> </LokiField>
<div class="col-2"> <LokiField class="col-2" label="Filter by transaction type">
<q-select :dark="theme=='dark'" <q-select :dark="theme=='dark'"
v-model="tx_type" v-model="tx_type"
float-label="Filter by transaction type"
:options="tx_type_options" :options="tx_type_options"
hide-underline
/> />
</div> </LokiField>
</div> </div>
<TxList :type="tx_type" :txid="tx_txid" /> <TxList :type="tx_type" :txid="tx_txid" />
@ -30,6 +31,7 @@
<script> <script>
import { mapState } from "vuex" import { mapState } from "vuex"
import TxList from "components/tx_list" import TxList from "components/tx_list"
import LokiField from "components/loki_field"
export default { export default {
data () { data () {
return { return {
@ -39,8 +41,10 @@ export default {
{label: "All", value: "all"}, {label: "All", value: "all"},
{label: "Incoming", value: "in"}, {label: "Incoming", value: "in"},
{label: "Outgoing", value: "out"}, {label: "Outgoing", value: "out"},
{label: "Pending incoming", value: "pool"}, {label: "Pending", value: "all_pending"},
{label: "Pending outgoing", value: "pending"}, {label: "Miner", value: "miner"},
{label: "Service Node", value: "snode"},
{label: "Governance", value: "gov"},
{label: "Failed", value: "failed"}, {label: "Failed", value: "failed"},
] ]
@ -52,7 +56,8 @@ export default {
}), }),
components: { components: {
TxList TxList,
LokiField
} }
} }

View file

@ -1,8 +1,6 @@
<template> <template>
<q-page padding> <q-page padding>
<AddressHeader :address="info.address" :title="info.name" />
<div class="row"> <div class="row">
<div class="infoBoxBalance"> <div class="infoBoxBalance">
@ -299,7 +297,7 @@ export default {
} }
} }
}, },
mounted() { mounted () {
const path = require("path") const path = require("path")
this.modals.key_image.export_path = path.join(this.data_dir, "gui") this.modals.key_image.export_path = path.join(this.data_dir, "gui")
this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export") this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export")

View file

@ -83,7 +83,7 @@ export default [
{ {
path: "", path: "",
component: () => component: () =>
import("pages/wallet/wallet") import("pages/wallet/txhistory")
}, },
{ {
path: "receive", path: "receive",
@ -100,11 +100,6 @@ export default [
component: () => component: () =>
import("pages/wallet/addressbook") import("pages/wallet/addressbook")
}, },
{
path: "txhistory",
component: () =>
import("pages/wallet/txhistory")
},
{ {
path: "servicenode", path: "servicenode",
component: () => component: () =>

66
src/statics/loki.svg Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 368" style="enable-background:new 0 0 1000 368;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#333333;}
.st3{fill:url(#SVGID_2_);}
.st4{fill:url(#SVGID_3_);}
.st5{fill:#00263A;}
.st6{fill:url(#SVGID_4_);}
.st7{fill:url(#SVGID_5_);}
.st8{fill:url(#SVGID_6_);}
.st9{fill:url(#SVGID_7_);}
.st10{fill:url(#SVGID_8_);}
.st11{fill:url(#SVGID_9_);}
.st12{fill:url(#SVGID_10_);}
.st13{fill:url(#SVGID_11_);}
.st14{fill:url(#SVGID_12_);}
.st15{fill:url(#SVGID_13_);}
.st16{fill:url(#SVGID_14_);}
.st17{fill:url(#SVGID_15_);}
.st18{fill:url(#SVGID_16_);}
.st19{fill:url(#SVGID_17_);}
.st20{opacity:6.000000e-02;}
.st21{opacity:4.000000e-02;fill:#FFFFFF;}
.st22{opacity:7.000000e-02;fill:#FFFFFF;}
.st23{fill:#008522;}
.st24{fill:#78BE20;}
.st25{fill:#005F61;}
.st26{fill:url(#SVGID_18_);}
</style>
<g>
<path class="st0" d="M366.6,78h37.1v178.9H497v32.7H366.6V78z"/>
<path class="st0" d="M619.8,74.5C683.3,74.5,728,120.8,728,184c0,63.1-44.7,109.5-108.2,109.5c-63.5,0-108.2-46.3-108.2-109.5
C511.6,120.8,556.3,74.5,619.8,74.5z M619.8,107.5c-42.8,0-70.1,32.7-70.1,76.5c0,43.5,27.3,76.5,70.1,76.5
c42.5,0,70.1-33,70.1-76.5C689.9,140.2,662.3,107.5,619.8,107.5z"/>
<path class="st0" d="M819.4,200.5L801,222v67.6h-37.1V78H801v100.9L883.8,78h46l-86,99.9l92.3,111.7h-45.7L819.4,200.5z"/>
<path class="st0" d="M960.9,78H998v211.6h-37.1V78z"/>
</g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="86.8402" y1="268.7968" x2="86.8402" y2="0.426">
<stop offset="0" style="stop-color:#78BE20"/>
<stop offset="0.1197" style="stop-color:#58AF21"/>
<stop offset="0.3682" style="stop-color:#199122"/>
<stop offset="0.486" style="stop-color:#008522"/>
<stop offset="0.6925" style="stop-color:#007242"/>
<stop offset="0.8806" style="stop-color:#006459"/>
<stop offset="1" style="stop-color:#005F61"/>
</linearGradient>
<polygon class="st1" points="132.1,268.8 0.3,137 136.9,0.4 173.3,36.8 73.1,137 168.5,232.4 "/>
</g>
<g>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="212.9564" y1="367.5197" x2="212.9564" y2="99.1484">
<stop offset="0" style="stop-color:#78BE20"/>
<stop offset="0.1197" style="stop-color:#58AF21"/>
<stop offset="0.3682" style="stop-color:#199122"/>
<stop offset="0.486" style="stop-color:#008522"/>
<stop offset="0.6925" style="stop-color:#007242"/>
<stop offset="0.8806" style="stop-color:#006459"/>
<stop offset="1" style="stop-color:#005F61"/>
</linearGradient>
<polygon class="st3" points="162.9,367.5 126.5,331.1 226.7,230.9 131.3,135.6 167.7,99.1 299.5,230.9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -29,6 +29,15 @@ export const resetWalletData = (state) => {
}) })
} }
export const resetWalletStatus = (state) => {
state.commit("set_wallet_data", {
status: {
code: 1,
message: null
}
})
}
export const resetPendingConfig = (state) => { export const resetPendingConfig = (state) => {
state.commit("set_app_data", { state.commit("set_app_data", {

View file

@ -3,18 +3,13 @@ export const isReady = (state) => {
const config_daemon = daemons[app.net_type] const config_daemon = daemons[app.net_type]
let target_height let target_height
if (config_daemon.type === "local" && !state.daemon.info.is_ready) { if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height) target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else { } else {
target_height = state.daemon.info.height target_height = state.daemon.info.height
} }
if (config_daemon.type === "local") { return state.wallet.info.height >= target_height - 1
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
return false
} }
export const isAbleToSend = (state) => { export const isAbleToSend = (state) => {
@ -22,18 +17,15 @@ export const isAbleToSend = (state) => {
const config_daemon = daemons[app.net_type] const config_daemon = daemons[app.net_type]
let target_height let target_height
if (config_daemon.type === "local" && !state.daemon.info.is_ready) { if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height) target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else { } else {
target_height = state.daemon.info.height target_height = state.daemon.info.height
} }
if (config_daemon.type === "local") { if (config_daemon.type === "local_remote") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else if (config_daemon.type === "local_remote") {
return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1 return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1
} else { } else {
return state.wallet.info.height >= target_height - 1 return state.wallet.info.height >= target_height - 1
} }
return false
} }

View file

@ -15,3 +15,6 @@ export const set_wallet_list = (state, data) => {
export const set_tx_status = (state, data) => { export const set_tx_status = (state, data) => {
state.tx_status = data state.tx_status = data
} }
export const set_stake_status = (state, data) => {
state.stake_status = data
}

View file

@ -5,7 +5,7 @@ export default {
}, },
config: { config: {
appearance: { appearance: {
theme: "light" theme: "dark"
} }
}, },
pending_config: { pending_config: {
@ -48,6 +48,11 @@ export default {
code: 0, code: 0,
message: "" message: ""
}, },
stake_status: {
code: 0,
message: "",
sending: false
},
daemon: { daemon: {
info: { info: {
alt_blocks_count: 0, alt_blocks_count: 0,

View file

@ -1,4 +1,8 @@
// import { validateAddress } from "./address_tools" /* eslint-disable prefer-promise-reject-errors */
export const greater_than_zero = (input) => {
return input > 0
}
export const payment_id = (input) => { export const payment_id = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64)) return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64))
@ -12,35 +16,24 @@ export const service_node_key = (input) => {
return input.length === 64 && /^[0-9A-Za-z]+$/.test(input) return input.length === 64 && /^[0-9A-Za-z]+$/.test(input)
} }
export const address = (input) => { export const address = (input, gateway) => {
if (!(/^[0-9A-Za-z]+$/.test(input))) return false if (!(/^[0-9A-Za-z]+$/.test(input))) return false
switch (input.substring(0, 4)) { // Validate the address
case "Sumo": return new Promise((resolve, reject) => {
case "RYoL": gateway.once("validate_address", (data) => {
case "Suto": if (data.address && data.address !== input) {
case "RYoT": reject()
return input.length === 99 } else {
if (data.valid) {
case "Subo": resolve()
case "Suso": } else {
return input.length == 98 reject()
}
case "RYoS": }
case "RYoU": })
return input.length == 99 gateway.send("wallet", "validate_address", {
address: input
case "Sumi": })
case "RYoN": })
case "Suti":
case "RYoE":
return input.length === 110
case "RYoK":
case "RYoH":
return input.length === 55
default:
return false
}
} }