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)
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)
Loki GUI wallet
---
### Building from source
#### Pre-requisite
- Download latest [Lokid](https://github.com/loki-project/loki/releases)
#### Commands
```
npm install -g quasar-cli
git clone https://github.com/ryo-currency/ryo-wallet
cd ryo-wallet
cp /path/to/ryo/binaries/ryod bin/
cp /path/to/ryo/binaries/ryo-wallet-rpc bin/
git clone https://github.com/loki-project/loki-electron-wallet
cd loki-electron-wallet
cp path_to_loki_binaries/lokid bin/
cp path_to_loki_binaries/loki-wallet-rpc bin/
npm install
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",
"description": "Modern GUI interface for Loki Currency",
"productName": "Loki Wallet Atom",
"cordovaId": "com.ryo-currency.ryo-gui-wallet",
"cordovaId": "com.lokinetwork.wallet",
"author": "Loki",
"private": true,
"scripts": {
@ -44,8 +44,8 @@
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.3.0",
"node-sass": "^4.9.3",
"quasar-cli": "^0.17.23",
"node-sass": "^4.11.0",
"quasar-cli": "^0.17.24",
"sass-loader": "^7.1.0",
"strip-ansi": "^3.0.1"
},

View File

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

View File

@ -7,6 +7,7 @@ const WebSocket = require("ws")
const os = require("os")
const fs = require("fs")
const path = require("path")
const objectAssignDeep = require("object-assign-deep")
export class Backend {
constructor (mainWindow) {
@ -38,7 +39,7 @@ export class Backend {
this.config_file = path.join(this.config_dir, "gui", "config.json")
const daemon = {
type: "local_remote",
type: "remote",
p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 22022,
rpc_bind_ip: "127.0.0.1",
@ -52,44 +53,50 @@ export class Backend {
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: {
data_dir: this.config_dir,
ws_bind_port: 12213,
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: {
rpc_bind_port: 18082,
log_level: 0
}
}
this.config_data = {
// Copy all the properties of defaults
...objectAssignDeep({}, this.defaults),
appearance: {
theme: "dark"
}
}
this.remotes = [
{
host: "doopool.xyz",
@ -195,6 +202,21 @@ export class Backend {
Object.keys(params).map(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", () => {
if (data.method == "save_config_init") {
this.startup()
@ -244,7 +266,8 @@ export class Backend {
startup () {
this.send("set_app_data", {
remotes: this.remotes
remotes: this.remotes,
defaults: this.defaults
})
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
// 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
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
switch (data.method) {
case "validate_address":
this.validateAddress(params.address)
break
case "list_wallets":
this.listWallets()
break
@ -191,6 +195,10 @@ export class WalletRPC {
this.closeWallet()
break
case "stake":
this.stake(params.password, params.amount, params.key, params.destination)
break
case "transfer":
this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book)
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) {
// Reset the status error
this.sendGateway("reset_wallet_error")
this.sendRPC("create_wallet", {
filename,
password,
@ -265,6 +305,7 @@ export class WalletRPC {
let timestamp = refresh_start_timestamp_or_height
timestamp = timestamp - (timestamp % 86400000) - 86400000
this.sendGateway("reset_wallet_error")
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) }
})
@ -278,6 +319,7 @@ export class WalletRPC {
}
seed = seed.trim().replace(/\s{2,}/g, " ")
this.sendGateway("reset_wallet_error")
this.sendRPC("restore_deterministic_wallet", {
filename,
password,
@ -289,25 +331,12 @@ export class WalletRPC {
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
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
this.wallet_state.open = true
this.finalizeNewWallet(filename)
// });
})
}
@ -352,6 +381,9 @@ export class WalletRPC {
}
importWallet (filename, password, import_path) {
// Reset the status error
this.sendGateway("reset_wallet_error")
// trim off suffix if exists
if (import_path.endsWith(".keys")) {
import_path = import_path.substring(0, import_path.length - ".keys".length)
@ -380,8 +412,8 @@ export class WalletRPC {
password
}).then((data) => {
if (data.hasOwnProperty("error")) {
fs.unlinkSync(destination)
fs.unlinkSync(destination + ".keys")
if (fs.existsSync(destination)) fs.unlinkSync(destination)
if (fs.existsSync(destination + ".keys")) fs.unlinkSync(destination + ".keys")
this.sendGateway("set_wallet_error", {status: data.error})
return
@ -393,6 +425,8 @@ export class WalletRPC {
this.wallet_state.open = true
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) {
this.sendGateway("reset_wallet_error")
this.sendRPC("open_wallet", {
filename,
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 = {}) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
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) }
if (data.result.hasOwnProperty("out")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.out) }
if (data.result.hasOwnProperty("pending")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pending) }
if (data.result.hasOwnProperty("failed")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.failed) }
if (data.result.hasOwnProperty("pool")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pool) }
const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov"]
types.forEach(type => {
if (data.result.hasOwnProperty(type)) {
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result[type]);
}
})
for (let i = 0; i < wallet.transactions.tx_list.length; i++) {
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-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-item>
<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>
<q-field v-if="mode=='edit'">
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
</q-field>
</div>
</q-modal-layout>
@ -119,7 +99,7 @@
<span class="vertical-middle q-ml-xs">Recent transactions with this address</span>
</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>
@ -137,6 +117,7 @@ import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressHeader from "components/address_header"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
import { payment_id, address } from "src/validators/common"
import { required } from "vuelidate/lib/validators"
export default {
@ -161,11 +142,27 @@ export default {
view_only: state => state.gateway.wallet.info.view_only,
is_ready (state) {
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: {
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 }
}
},
@ -235,7 +232,8 @@ export default {
components: {
AddressHeader,
Identicon,
TxList
TxList,
LokiField
}
}
</script>
@ -243,12 +241,14 @@ export default {
<style lang="scss">
.address-book-details {
.q-field {
margin: 0 10px 20px;
}
.q-checkbox.star-entry .q-checkbox-icon {
font-size:40px;
margin-left: 10px;
.address-book-modal {
> .loki-field {
margin-top: 16px;
}
.star-entry {
padding: 4px;
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}">
<q-modal v-model="isVisible" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
@ -23,6 +23,7 @@
<AddressHeader :address="address.address"
:title="address.address_index == 0 ? 'Primary address' : 'Sub-address (Index '+address.address_index+')'"
:extra="'You have '+(address.used?'used':'not used')+' this address'"
:showCopy="false"
/>
@ -87,7 +88,7 @@
</div>
<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>

View File

@ -1,17 +1,15 @@
<template>
<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-tile label>{{ title }}</q-item-tile>
<q-item-tile class="monospace break-all" sublabel>{{ address }}</q-item-tile>
<q-item-tile sublabel class="title">{{ title }}</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="extra" sublabel>{{ extra }}</q-item-tile>
<q-item-tile v-if="extra" sublabel class="extra">{{ extra }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-side v-if="showCopy">
<q-btn
color="primary" style="width:25px;"
color="primary"
style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
@ -28,11 +26,6 @@
@click.native="copyAddress(address, $event)">
<q-item-main label="Copy address" />
</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-context-menu>
@ -60,6 +53,11 @@ export default {
extra: {
type: String,
required: false
},
showCopy: {
type: Boolean,
required: false,
default: true
}
},
data () {
@ -125,7 +123,20 @@ export default {
.q-item-main {
.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>
<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'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<template v-if="config_daemon.type !== 'local'">
<div>Remote: {{ daemon.info.height }}</div>
</template>
<template v-if="config_daemon.type !== 'local'">
<div>Remote: {{ daemon.info.height }}</div>
</template>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
<div>{{ status }}</div>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</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: wallet_pct+'%' }"></div>
</div>
@ -36,7 +39,7 @@ export default {
return this.config.daemons[this.config.app.net_type]
},
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)
else
return this.daemon.info.height
@ -64,26 +67,31 @@ export default {
},
status(state) {
if(this.config_daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) {
return "Syncing..."
if(this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..."
return "scanning"
} else {
return "Ready"
return "ready"
}
} else {
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) {
return "Syncing..."
return "syncing"
} else {
return "Ready"
return "ready"
}
}
return
}
}),
filters: {
upperCase: function (status) {
return status.toUpperCase();
}
},
data () {
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>
<div>
<q-btn class="menu" icon="menu" label="" size="md" flat>
<q-btn class="menu" icon="menu" size="md" flat>
<q-popover>
<q-list separator link>
<q-item v-close-overlay @click.native="switchWallet" v-if="!disableSwitchWallet">
@ -31,7 +31,7 @@
<q-modal minimized ref="aboutModal">
<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">Copyright (c) 2018, Ryo Currency Project</p>
@ -44,7 +44,8 @@
<p>
<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://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>
</div>

View File

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

View File

@ -1,9 +1,9 @@
<template>
<div class="settings-general">
<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" label="Local Daemon Only" /></div>
<div><q-radio v-model="config_daemon.type" val="remote" label="Remote Daemon Only" /></div>
</div>
<p v-if="config_daemon.type == 'local_remote'">
@ -12,122 +12,131 @@
<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.
</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.
</p>
<q-field v-if="config_daemon.type != 'remote'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config_daemon.rpc_bind_ip" float-label="Local Daemon IP"
:dark="theme=='dark'" disable />
</div>
<div class="col-4">
<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'" />
</div>
<template v-if="config_daemon.type != 'remote'">
<div class="row pl-sm">
<LokiField class="col-8" label="Local Daemon IP" disable>
<q-input
v-model="config_daemon.rpc_bind_ip"
:placeholder="daemon_defaults.rpc_bind_ip"
:dark="theme=='dark'"
disable
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>
</q-field>
</template>
<q-field v-if="config_daemon.type != 'local'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config_daemon.remote_host" float-label="Remote Node Host" :dark="theme=='dark'" />
</div>
<div class="col-4">
<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'" />
</div>
<template v-if="config_daemon.type != 'local'">
<div class="row q-mt-md pl-sm">
<LokiField class="col-8" label="Remote Node Host">
<q-input
v-model="config_daemon.remote_host"
:placeholder="daemon_defaults.remote_host"
:dark="theme=='dark'"
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>
</q-field>
</template>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<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 />
</div>
<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 class="col q-mt-md">
<LokiField label="Data Storage Path" disable-hover>
<q-input v-model="config.app.data_dir" disable :dark="theme=='dark'" hide-underline/>
<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>
</LokiField>
</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 gutter-sm">
<div class="col-6">
<q-input v-model="config_daemon.log_level" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
<div class="col-6">
<q-input v-model="config.wallet.log_level" :dark="theme=='dark'"
float-label="Wallet Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
</div>
</q-field>
<div class="row pl-sm q-mt-sm">
<LokiField class="col-6" label="Daemon Log Level" :disable="is_remote">
<q-input v-model="config_daemon.log_level" :placeholder="toString(daemon_defaults.log_level)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
<LokiField class="col-6" label="Wallet Log Level">
<q-input v-model="config.wallet.log_level" :placeholder="toString(defaults.wallet.log_level)" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
</div>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config_daemon.in_peers" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Max Incoming Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config_daemon.out_peers" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Max Outgoing Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config_daemon.limit_rate_up" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Limit Upload Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config_daemon.limit_rate_down" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Limit Download Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config_daemon.p2p_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon P2P Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config_daemon.zmq_rpc_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon ZMQ Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.app.ws_bind_port" :dark="theme=='dark'"
float-label="Internal Wallet Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.wallet.rpc_bind_port" :disable="config_daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Wallet RPC Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
</q-field>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" label="Max Incoming Peers" :disable="is_remote">
<q-input v-model="config_daemon.in_peers" :placeholder="toString(daemon_defaults.in_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Max Outgoing Peers" :disable="is_remote">
<q-input v-model="config_daemon.out_peers" :placeholder="toString(daemon_defaults.out_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Limit Upload Rate" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_up" :placeholder="toString(daemon_defaults.limit_rate_up)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Limit Download Rate" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_down" :placeholder="toString(daemon_defaults.limit_rate_down)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" label="Daemon P2P Port" :disable="is_remote">
<q-input v-model="config_daemon.p2p_bind_port" :placeholder="toString(daemon_defaults.p2p_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Daemon ZMQ Port" :disable="is_remote">
<q-input v-model="config_daemon.zmq_rpc_bind_port" :placeholder="toString(daemon_defaults.zmq_rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Internal Wallet Port">
<q-input v-model="config.app.ws_bind_port" :placeholder="toString(defaults.app.ws_bind_port)" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Wallet RPC Port" :disable="is_remote">
<q-input v-model="config.wallet.rpc_bind_port" :placeholder="toString(defaults.wallet.rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
</div>
<q-field helper="Choose a network" label="Network" orientation="vertical">
<q-option-group
type="radio"
@ -146,21 +155,23 @@
<script>
import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default {
name: "SettingsGeneral",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
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_daemon (state) {
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: {
selectPath () {
@ -169,19 +180,26 @@ export default {
setDataPath (file) {
this.config.app.data_dir = file.target.files[0].path
},
loadPreset () {
if (!this.remotes || this.remotes.length === 0) return;
setPreset (option) {
if (!option) return;
const { host, port } = this.remotes[this.select];
this.config_daemon.remote_host = host;
this.config_daemon.remote_port = port;
const { host, port } = option;
if (host) this.config_daemon.remote_host = host;
if (port) this.config_daemon.remote_port = port;
},
toString (value) {
if (!value && typeof value !== "number") return ""
return String(value);
}
},
data () {
return {
select: 0,
}
},
components: {
LokiField,
}
}
</script>
@ -203,8 +221,14 @@ export default {
padding: 0;
}
.presets {
margin-top: 20px;
.row.pl-sm {
> * + * {
padding-left: 16px;
}
}
.remote-dropdown {
padding: 0 !important;
}
}
</style>

View File

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

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="tx-list">
<template v-if="tx_list_paged.length === 0">
<p class="q-pa-md q-mb-none">No transactions found</p>
@ -8,23 +8,22 @@
<template v-else>
<q-infinite-scroll :handler="loadMore" ref="scroller">
<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">
<q-item-side>
<TxTypeIcon :type="tx.type" />
<q-item-side class="type">
<div>{{ tx.type | typeToString }}</div>
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ tx.txid }}</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-tile label>
<q-item-main class="main">
<q-item-tile class="amount" label>
<FormatLoki :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>
<timeago :datetime="tx.timestamp*1000" :auto-update="60">
</timeago>
<q-item-tile sublabel>{{ tx.txid }}</q-item-tile>
</q-item-main>
<q-item-side class="meta">
<q-item-tile label>
<timeago :datetime="tx.timestamp*1000" :auto-update="60" />
</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-side>
<q-context-menu>
@ -105,7 +104,7 @@ export default {
theme: state => state.gateway.app.config.appearance.theme,
current_height: state => state.gateway.daemon.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 () {
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: {
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) => {
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
return valid
}
@ -242,4 +280,41 @@ export default {
</script>
<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>

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
@import '~variables'
@font-face {
font-family: 'RobotoMono-Light';
@ -12,16 +13,11 @@
background: #63c9f3;
}
:root {
--q-color-primary: #497dc6;
}
* {
scrollbar-track-color: #fff;
scrollbar-arrow-color: #fff;
&::-webkit-scrollbar-corner { background-color: #fff;}
&::-webkit-scrollbar-track-piece { background-color: #fff;}
scrollbar-track-color: #111;
scrollbar-arrow-color: #111;
&::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #111;}
scrollbar-face-color: #646464;
scrollbar-base-color: #646464;
@ -81,7 +77,7 @@ footer,
}
.q-item-sublabel {
color: #121212;
colr: #cecece;
}
.advanced-options-label .q-item-side-right {
@ -89,14 +85,15 @@ footer,
text-align: left;
}
.q-layout-page {
min-height: 0 !important;
.q-layout {
background: $loki-black-80;
color:white;
}
.q-layout-header {
background: white;
background: #0A0A0A;
box-shadow: none;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid #333;
height: 48px;
.q-toolbar-title {
@ -154,7 +151,10 @@ footer,
}
}
}
.q-layout-page {
min-height: 0 !important;
}
.infoBox {
@ -169,7 +169,7 @@ footer,
right: 10px;
top: 10px;
width: 64px;
color: #212529;
color: #cecece;
svg {
width: 60px;
@ -183,7 +183,7 @@ footer,
.text {
font-size: 13px;
margin-top: 8px;
color: #212529;
color: #cecece;
text-transform: uppercase;
}
@ -191,7 +191,7 @@ footer,
font-size: 24px;
margin-top: 4px;
font-weight: 800;
color: #212529;
color: #cecece;
font-weight:300;
}
}
@ -213,27 +213,44 @@ footer,
background-position: center center;
background-repeat: no-repeat;
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;
}
.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;
background: #fff;
box-shadow: none;
font-size: 12px;
.status-line {
padding: 8px;
margin-bottom: 5px;
div {
display: inline-block;
padding: 0 8px;
.status {
flex: 1;
span {
margin-right: 4px;
}
.status-text {
font-weight: bold;
text-transform: uppercase;
}
.ready {
color: $loki-green-solid;
}
.scanning, .syncing {
color: goldenrod;
}
}
div:last-child {
float:right;
div {
padding: 0 8px;
}
}
@ -244,17 +261,27 @@ footer,
position: absolute;
bottom: 0;
left: 0;
-webkit-transition: width 0.5s ease-out;
transition: width 0.5s ease-out;
}
div:first-child {
background-color: goldenrod;
background-color: $loki-green-dark-solid;
}
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 {
.meta {
text-align:right;
}
.q-item.tx-in,
.q-item.tx-pool {
.q-icon {
color: #333;
}
.q-item-label {
color: green;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: green;
&:before {
content: "+";
color: green;
}
}
.q-item.tx-pool,
.q-item.tx-miner,
.q-item.tx-snode,
.q-item.tx-gov {
.amount span {
color: #43bd43;
&:before {
content: "+";
color: #43bd43;
}
}
}
.q-item.tx-out,
.q-item.tx-pending {
.q-icon {
color: #333;
}
.q-item-label {
color: purple;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: purple;
&:before {
content: "-";
color: purple;
}
}
.amount span {
color: white;
&:before {
content: "-";
font-weight: bold;
color: white;
}
}
}
.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;
}
&, * {
scrollbar-track-color: #111;
scrollbar-arrow-color: #111;
&::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #111;}
}
.q-popover {
background: #222;
color:#cecece;
.q-item-sublabel {
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;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid #333;
}
}
.q-list-dark .q-item,
.q-item-dark .q-item,
.q-list-header {
color: #cecece;
.header-popover.q-popover {
background: $primary;
color: white;
.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,
.q-stepper-step-content{
color: #cecece;
.modal.minimized {
.q-input {
.q-input-target {
color: #cecece;
}
&.q-if:hover:before {
color: #cecece;
}
}
}
.modal {
.q-popover {
background: #222;
.modal-content,
.modal-body {
background: $loki-black-80;
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-input {
.q-input-target {
color: #cecece;
}
&.q-if:hover:before {
.q-layout-header {
.q-toolbar {
&>* {
color: #cecece;
}
}
}
.modal {
.modal-content,
.modal-body {
background: #111;
color:#cecece;
}
.q-layout-header {
background: #111;
box-shadow: none;
border-bottom: 1px solid #333;
.q-toolbar {
&>* {
color: #cecece;
}
}
}
}
.tx-list {
.q-item.tx-in,
.q-item.tx-pool {
.q-icon {
color: #cecece;
}
.q-item-label {
color: lightgreen;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: lightgreen;
&:before {
content: "+";
color: lightgreen;
}
}
}
}
}
.q-item.tx-out,
.q-item.tx-pending {
.q-icon {
color: #cecece;
}
.q-item-label {
color: mediumpurple;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: mediumpurple;
&:before {
content: "-";
color: mediumpurple;
}
}
}
}
}
}
.q-layout-footer.status-footer {
.status-bars {
div:first-child {
background-color: goldenrod;
}
div:last-child {
background-color: #497dc6;
}
}
}
}
.loki-green {
background: $loki-green;
color: white;
}
.startup-icons {
.solid {
color:$loki-green-solid;
g,path {
fill: $loki-green-solid;
}
}
}
.hr-separator {
height: 2px;
background: $secondary;
opacity: 0.4;
}
.navigation {
.q-btn {
color: white;
background: $loki-black-90;
}
.router-link-exact-active > .q-btn {
background: $loki-green;
}
}
.wallet-list {
.q-item-label {
color: white;
}
.q-item {
background: $secondary;
.wallet-icon {
color: $tertiary;
g,path {
fill: $tertiary;
}
}
}
.q-item:hover {
background: $primary !important;
.wallet-icon {
color:$loki-green-solid;
g,path {
fill: $loki-green-solid;
}
}
.q-item-sublabel {
color: white
}
}
}
.receive {
.q-list-header {
color: #FFFFFF;
}
.q-item-sublabel {
color: $loki-black-50;
}
.q-item-separator-component {
background-color: $secondary;
opacity: 0.4;
}
.item-group {
background: #313131;
-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
// to match your app"s branding.
$primary = #027be3
$secondary = #26A69A
$tertiary = #555
$primary = $loki-green
$secondary = $loki-black-90
$tertiary = $loki-black-80
$neutral = #E0E1E2
$positive = #21BA45
$negative = #DB2828
$info = #31CCEC
$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 { Notify, Dialog, Loading, LocalStorage } from "quasar"
import { EventEmitter } from "events"
import { SCEE } from "./SCEE-Node"
export class Gateway {
export class Gateway extends EventEmitter {
constructor (app, router) {
super()
this.app = app
this.router = router
this.token = null
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", {
config: {
appearance: {
@ -98,6 +100,9 @@ export class Gateway {
!decrypted_data.hasOwnProperty("data")) { return }
switch (decrypted_data.event) {
case "set_valid_address":
this.emit("validate_address", decrypted_data.data)
break
case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data)
break
@ -111,10 +116,18 @@ export class Gateway {
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data)
break
case "reset_wallet_error":
this.app.store.dispatch("gateway/resetWalletStatus")
break
case "set_tx_status":
this.app.store.commit("gateway/set_tx_status", decrypted_data.data)
break
case "set_stake_status":
this.app.store.commit("gateway/set_stake_status", decrypted_data.data)
break
case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break

View File

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

View File

@ -3,31 +3,57 @@
<q-layout-header class="shift-title">
<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-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>
<router-view />
</keep-alive>
@ -39,14 +65,18 @@
</template>
<script>
const { clipboard } = require("electron")
import { openURL } from "quasar"
import { mapState } from "vuex"
import WalletDetails from "components/wallet_details"
import FormatLoki from "components/format_loki"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
export default {
name: "LayoutDefault",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
data() {
return {
@ -58,10 +88,40 @@ export default {
},
components: {
StatusFooter,
MainMenu
MainMenu,
WalletDetails
}
}
</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>

View File

@ -2,32 +2,7 @@
<q-page>
<div class="init-screen-page text-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">
<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>
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="startup-icons q-mt-xl q-mb-lg">
<div ref="backend">
@ -149,50 +124,21 @@ export default {
&>div {
display: inline-block;
margin: 0 15px;
color: lightgrey;
color: #444;
g,path {
fill: lightgrey;
}
}
.solid {
color: var(--q-color-primary);
g,path {
fill: var(--q-color-primary);
fill: #444;
}
}
.pulse {
color:black;
color:#cecece;
opacity: 0.6;
animation: fade 2s infinite;
g,path {
fill:black;
}
}
}
.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;
}
fill:#cecece;
}
}
}
@keyframes fade {
0%,100% { opacity: 0.3 }
50% { opacity: 0.6 }

View File

@ -2,32 +2,7 @@
<q-page>
<div class="init-screen-page text-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">
<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>
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" />

View File

@ -1,56 +1,27 @@
<template>
<q-page>
<q-page class="welcome">
<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">
<defs>
<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="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 class="welcome-container">
<img src="statics/loki.svg" height="100" class="q-mb-md">
<div>Version: ATOM v{{version}}-v{{daemonVersion}}</div>
<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
v-model="choose_theme"
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>
<p class="q-mt-md">More languages coming soon</p>
</div>
</q-step>
@ -59,37 +30,6 @@
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</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-layout-footer class="no-shadow q-pa-sm">
@ -130,23 +70,11 @@ export default {
}),
data() {
return {
choose_theme: "light",
choose_lang: "EN",
version: "",
daemonVersion: ""
}
},
watch: {
choose_theme: function (val) {
this.$store.commit("gateway/set_app_data", {
config: {
appearance: {
theme: val
}
}
});
}
},
mounted () {
this.version = version
@ -180,6 +108,21 @@ export default {
<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 {
padding: 10px 30px 10px 20px;

View File

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

View File

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

View File

@ -137,7 +137,18 @@ export default {
validations: {
wallet: {
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 },
refresh_start_height: { numeric }
}

View File

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

View File

@ -1,27 +1,31 @@
<template>
<q-page>
<q-list link no-border :dark="theme=='dark'">
<q-list class="wallet-list" link no-border :dark="theme=='dark'">
<template v-if="wallets.list.length">
<q-list-header>Open wallet</q-list-header>
<q-item v-for="(wallet, index) in wallets.list" @click.native="openWallet(wallet)">
<div class="header row justify-between items-center">
<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>
<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-main>
<q-item-tile label>{{ wallet.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile>
</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-list link separator style="min-width: 150px; max-height: 300px;">
@ -34,33 +38,15 @@
@click.native="copyAddress(wallet.address, $event)">
<q-item-main label="Copy address" />
</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-context-menu>
</q-item>
<q-item-separator />
</template>
<q-item @click.native="createNewWallet()">
<q-item-main label="Create new wallet" />
</q-item>
<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" />
<template v-else>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</template>
</q-list>
@ -76,7 +62,27 @@ export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
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: {
openWallet(wallet) {
@ -176,5 +182,28 @@ export default {
}
</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>

View File

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

View File

@ -1,39 +1,43 @@
<template>
<q-page>
<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-page class="receive">
<q-list link no-border :dark="theme=='dark'">
<q-list-header>My primary address</q-list-header>
<q-item v-for="(address, index) in address_list.primary" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" ref="primaryIdenticon" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Primary address</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<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>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Primary address</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</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-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@ -45,36 +49,47 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</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-context-menu>
</q-item>
</q-list>
<template v-if="address_list.used.length">
<q-list-header>My used addresses</q-list-header>
<q-item v-for="(address, index) in address_list.used" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" :ref="`${index}-usedIdenticon`" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-list class="item-group" no-border v-for="address in address_list.used" @click.native="details(address)" :key="address.address">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</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-list link separator style="min-width: 150px; max-height: 300px;">
@ -87,38 +102,32 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</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-context-menu>
</q-item>
</q-list>
</template>
<template v-if="address_list.unused.length">
<q-list-header>My unused addresses</q-list-header>
<q-item v-for="(address, index) in address_list.unused" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" :ref="`${index}-unusedIdenticon`" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-list class="item-group" no-border v-for="address in address_list.unused" @click.native="details(address)" :key="address.address">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
@ -131,15 +140,10 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</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-context-menu>
</q-item>
</q-list>
</template>
</q-list>
@ -147,10 +151,8 @@
</q-page>
</template>
<style>
</style>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import Identicon from "components/identicon"
@ -160,6 +162,18 @@ export default {
theme: state => state.gateway.app.config.appearance.theme,
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: {
details (address) {
this.$refs.addressDetails.address = address;
@ -187,3 +201,33 @@ export default {
}
}
</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>
<q-page>
<q-page class="send">
<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">
View-only mode. Please load full wallet in order to send coins.
</div>
</template>
<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="row items-end gutter-md">
<div class="col">
<q-field class="q-ma-none">
<q-input v-model="newTx.amount" float-label="Amount" :dark="theme=='dark'"
type="number" min="0" :max="unlocked_balance / 1e9" />
</q-field>
<div class="row gutter-md">
<!-- Amount -->
<div class="col-6">
<LokiField label="Amount" :error="$v.newTx.amount.$error">
<q-input v-model="newTx.amount"
:dark="theme=='dark'"
type="number"
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>
<q-btn @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All coins</q-btn>
<!-- Priority -->
<div class="col-6">
<LokiField label="Priority">
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
:options="priorityOptions"
hide-underline
/>
</LokiField>
</div>
</div>
<q-item class="q-pa-none">
<q-item-side>
<Identicon :address="newTx.address" menu />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newTx.address" float-label="Address"
:dark="theme=='dark'"
@blur="$v.newTx.address.$touch"
:error="$v.newTx.address.$error"
/>
</q-field>
</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>
<!-- Address -->
<div class="col q-mt-sm">
<LokiField label="Address" :error="$v.newTx.address.$error">
<q-input v-model="newTx.address"
:dark="theme=='dark'"
@blur="$v.newTx.address.$touch"
:placeholder="address_placeholder"
hide-underline
/>
<q-btn color="secondary" :text-color="theme=='dark'?'white':'dark'" to="addressbook">Contacts</q-btn>
</LokiField>
</div>
<!-- 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-checkbox v-model="newTx.address_book.save" label="Save to address book" :dark="theme=='dark'" />
</q-field>
<div v-if="newTx.address_book.save">
<q-field>
<q-input v-model="newTx.address_book.name" float-label="Name" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="newTx.address_book.description" type="textarea" rows="2" float-label="Notes" :dark="theme=='dark'" />
</q-field>
<LokiField label="Name" optional>
<q-input v-model="newTx.address_book.name"
:dark="theme=='dark'"
placeholder="Name that belongs to this address"
hide-underline
/>
</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>
<q-field class="q-pt-sm">
<q-btn
class="send-btn"
:disable="!is_able_to_send"
color="primary" @click="send()" label="Send" />
</q-field>
@ -116,8 +110,9 @@
<script>
import { mapState } from "vuex"
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 LokiField from "components/loki_field"
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
@ -130,6 +125,11 @@ export default {
},
is_able_to_send (state) {
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 () {
@ -158,9 +158,21 @@ export default {
newTx: {
amount: {
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 }
}
},
@ -291,17 +303,23 @@ export default {
message: "Sending transaction",
sending: true
})
let newTx = objectAssignDeep.noMutate(this.newTx, {password})
const newTx = objectAssignDeep.noMutate(this.newTx, {password})
this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => {
})
}
},
components: {
Identicon
Identicon,
LokiField
}
}
</script>
<style>
<style lang="scss">
.send {
.send-btn {
width: 200px;
}
}
</style>

View File

@ -1,59 +1,44 @@
<template>
<q-page class="service-node-page">
<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">
<q-field>
<q-input v-model="serviceNode.key" float-label="Service Node Key"
:dark="theme=='dark'"
@blur="$v.serviceNode.key.$touch"
:error="$v.serviceNode.key.$error"
/>
</q-field>
<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>
<LokiField label="Service Node Key" :error="$v.service_node.key.$error">
<q-input v-model="service_node.key"
:dark="theme=='dark'"
@blur="$v.service_node.key.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<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>
<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-btn
:disable="!is_able_to_send"
@ -62,9 +47,9 @@
</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-inner-loading> -->
</q-inner-loading>
</template>
@ -75,81 +60,172 @@
const { clipboard } = require("electron")
import { mapState } from "vuex"
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 LokiField from "components/loki_field"
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
address_list: state => state.gateway.wallet.address_list,
stake_status: state => state.gateway.stake_status,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
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 () {
return {
staking: false,
serviceNode: {
service_node: {
key: "",
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: {
serviceNode: {
service_node: {
key: { required, service_node_key },
amount: {
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: {
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
isOurAddress (address) {
const { primary, used, unused } = this.address_list
const addresses = [...primary, ...used, ...unused].map(o => o.address);
console.log(addresses);
return addresses.includes(address);
},
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({
type: "negative",
timeout: 1000,
message: "Amount cannot be negative"
})
return
} else if(this.serviceNode.amount == 0) {
} else if(this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount must be greater than zero"
})
return
} else if(this.serviceNode.amount > this.unlocked_balance / 1e9) {
} else if(this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Not enough unlocked balance"
})
return
} else if (this.$v.serviceNode.amount.$error) {
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
@ -158,15 +234,6 @@ export default {
return
}
if (this.$v.serviceNode.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
this.$q.dialog({
title: "Stake",
message: "Enter wallet password to continue.",
@ -183,34 +250,37 @@ export default {
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
// this.$store.commit("gateway/set_tx_status", {
// code: 1,
// message: "Sending transaction",
// sending: true
// })
// let newTx = objectAssignDeep.noMutate(this.newTx, {password})
// this.$gateway.send("wallet", "transfer", newTx)
this.$store.commit("gateway/set_stake_status", {
code: 1,
message: "Staking...",
sending: true
})
const service_node = objectAssignDeep.noMutate(this.service_node, {password})
this.$gateway.send("wallet", "stake", {
...service_node,
destination: service_node.award_address,
})
}).catch(() => {
})
}
},
components: {
Identicon,
LokiField
}
}
</script>
<style lang="scss">
.service-node-page {
.q-item {
padding-left: 0;
padding-right: 0;
}
.recepient-address {
margin-bottom: 8px;
font-size: 1rem;
.address-type {
margin-top: 4px;
font-size: 13px;
font-weight: 400;
text-align: right;
&.not-ours {
font-weight: bold;
}
}
}
</style>

View File

@ -1,26 +1,27 @@
<template>
<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">
<q-icon name="history" size="24px" /> Transaction history
Transactions
</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"
stack-label="Filter by txid"
: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'"
v-model="tx_type"
float-label="Filter by transaction type"
:options="tx_type_options"
hide-underline
/>
</div>
</LokiField>
</div>
<TxList :type="tx_type" :txid="tx_txid" />
@ -30,6 +31,7 @@
<script>
import { mapState } from "vuex"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
export default {
data () {
return {
@ -39,8 +41,10 @@ export default {
{label: "All", value: "all"},
{label: "Incoming", value: "in"},
{label: "Outgoing", value: "out"},
{label: "Pending incoming", value: "pool"},
{label: "Pending outgoing", value: "pending"},
{label: "Pending", value: "all_pending"},
{label: "Miner", value: "miner"},
{label: "Service Node", value: "snode"},
{label: "Governance", value: "gov"},
{label: "Failed", value: "failed"},
]
@ -52,7 +56,8 @@ export default {
}),
components: {
TxList
TxList,
LokiField
}
}

View File

@ -1,8 +1,6 @@
<template>
<q-page padding>
<AddressHeader :address="info.address" :title="info.name" />
<div class="row">
<div class="infoBoxBalance">
@ -299,7 +297,7 @@ export default {
}
}
},
mounted() {
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")

View File

@ -83,7 +83,7 @@ export default [
{
path: "",
component: () =>
import("pages/wallet/wallet")
import("pages/wallet/txhistory")
},
{
path: "receive",
@ -100,11 +100,6 @@ export default [
component: () =>
import("pages/wallet/addressbook")
},
{
path: "txhistory",
component: () =>
import("pages/wallet/txhistory")
},
{
path: "servicenode",
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) => {
state.commit("set_app_data", {

View File

@ -3,18 +3,13 @@ export const isReady = (state) => {
const config_daemon = daemons[app.net_type]
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)
} else {
target_height = state.daemon.info.height
}
if (config_daemon.type === "local") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
return false
return state.wallet.info.height >= target_height - 1
}
export const isAbleToSend = (state) => {
@ -22,18 +17,15 @@ export const isAbleToSend = (state) => {
const config_daemon = daemons[app.net_type]
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)
} else {
target_height = state.daemon.info.height
}
if (config_daemon.type === "local") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else if (config_daemon.type === "local_remote") {
if (config_daemon.type === "local_remote") {
return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1
} else {
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) => {
state.tx_status = data
}
export const set_stake_status = (state, data) => {
state.stake_status = data
}

View File

@ -5,7 +5,7 @@ export default {
},
config: {
appearance: {
theme: "light"
theme: "dark"
}
},
pending_config: {
@ -48,6 +48,11 @@ export default {
code: 0,
message: ""
},
stake_status: {
code: 0,
message: "",
sending: false
},
daemon: {
info: {
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) => {
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)
}
export const address = (input) => {
export const address = (input, gateway) => {
if (!(/^[0-9A-Za-z]+$/.test(input))) return false
switch (input.substring(0, 4)) {
case "Sumo":
case "RYoL":
case "Suto":
case "RYoT":
return input.length === 99
case "Subo":
case "Suso":
return input.length == 98
case "RYoS":
case "RYoU":
return input.length == 99
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110
case "RYoK":
case "RYoH":
return input.length === 55
default:
return false
}
// Validate the address
return new Promise((resolve, reject) => {
gateway.once("validate_address", (data) => {
if (data.address && data.address !== input) {
reject()
} else {
if (data.valid) {
resolve()
} else {
reject()
}
}
})
gateway.send("wallet", "validate_address", {
address: input
})
})
}