start/stop node

This commit is contained in:
Richard Ramos 2021-08-18 15:59:52 -04:00
parent fd0b897cc0
commit ed04a04db7
No known key found for this signature in database
GPG Key ID: 80D4B01265FDFE8F
74 changed files with 3423 additions and 98 deletions

3
.gitignore vendored
View File

@ -29,4 +29,5 @@ resources.qrc
status-react-translations/
/.update.timestamp
notarization.log
status-desktop.log
DesktopNode
status-node.log

View File

@ -34,7 +34,9 @@ BUILD_SYSTEM_DIR := vendor/nimbus-build-system
run-macos \
run-windows \
status-go \
update
update \
clean-status-go \
rebuild-status-go \
ifeq ($(NIM_PARAMS),)
# "variables.mk" was not included, so we update the submodules.
@ -186,6 +188,11 @@ $(STATUSGO): | deps
+ cd vendor/status-go && \
$(MAKE) statusgo-shared-library $(HANDLE_OUTPUT)
clean-status-go:
rm -f vendor/status-go/build/bin/libstatus.*
rebuild-status-go: clean-status-go status-go
FLEETS := fleets.json
$(FLEETS):
echo -e $(BUILD_MSG) "Getting latest $(FLEETS)"

37
src/app/node/core.nim Normal file
View File

@ -0,0 +1,37 @@
import NimQml, chronicles
import ../../status/signals/types
import ../../status/[status, node]
import ../../status/types as status_types
import ../../eventemitter
import view
logScope:
topics = "node"
type NodeController* = ref object
status*: Status
view*: NodeView
variant*: QVariant
proc newController*(status: Status, fleetConfig: string): NodeController =
result = NodeController()
result.status = status
result.view = newNodeView(status, fleetConfig)
result.variant = newQVariant(result.view)
proc delete*(self: NodeController) =
delete self.variant
delete self.view
proc init*(self: NodeController) =
self.status.events.on(SignalType.Stats.event) do (e:Args):
self.view.setStats(StatsSignal(e).stats)
self.status.events.on(SignalType.NodeStarted.event) do (e:Args):
self.view.setNodeActive(true)
self.status.events.on(SignalType.NodeCrashed.event) do (e:Args):
self.view.setNodeActive(false)
self.status.events.on(SignalType.NodeStopped.event) do (e:Args):
self.view.setNodeActive(false)

78
src/app/node/view.nim Normal file
View File

@ -0,0 +1,78 @@
import NimQml, chronicles, strutils, json
import ../../status/[status, node, types, settings]
import ../../status/signals/types as signal_types
import ../../status/libstatus/accounts/constants
logScope:
topics = "node-view"
QtObject:
type NodeView* = ref object of QObject
status*: Status
stats*: Stats
fleetConfig: string
nodeActive*: bool
proc setup(self: NodeView) =
self.QObject.setup
proc newNodeView*(status: Status, fleetConfig: string): NodeView =
new(result)
result.status = status
result.nodeActive = false
result.fleetConfig = fleetConfig
result.setup
proc delete*(self: NodeView) =
self.QObject.delete
proc getDataDir(self:NodeView): string {.slot.} = DATADIR
QtProperty[string] dataDir:
read = getDataDir
proc getNodeActive(self:NodeView): bool {.slot.} = self.nodeActive
proc nodeActiveChanged(self:NodeView, value:bool) {.signal.}
proc setNodeActive*(self:NodeView, value: bool) {.slot.} =
self.nodeActive = value
self.nodeActiveChanged(value)
QtProperty[bool] nodeActive:
read = getNodeActive
notify = nodeActiveChanged
proc getFleetConfig(self:NodeView): string {.slot.} = self.fleetConfig
QtProperty[string] fleetConfig:
read = getFleetConfig
proc statsChanged*(self: NodeView) {.signal.}
proc setStats*(self: NodeView, stats: Stats) =
self.stats = stats
self.statsChanged()
proc resetStats(self: NodeView) =
self.setStats(Stats())
proc uploadRate*(self: NodeView): string {.slot.} = $self.stats.uploadRate
QtProperty[string] uploadRate:
read = uploadRate
notify = statsChanged
proc downloadRate*(self: NodeView): string {.slot.} = $self.stats.downloadRate
QtProperty[string] downloadRate:
read = downloadRate
notify = statsChanged
proc startNode*(self: NodeView, jsonConfig: string) {.slot.} =
self.status.settings.startNode(jsonConfig)
proc stopNode*(self: NodeView) {.slot.} =
self.status.settings.stopNode()
self.setNodeActive(false)
self.resetStats()

5
src/status/constants.nim Normal file
View File

@ -0,0 +1,5 @@
import libstatus/accounts/constants
export DATADIR
export STATUSGODIR
export KEYSTOREDIR

View File

@ -0,0 +1,13 @@
import json, os, uuids, json_serialization, chronicles, strutils
from status_go import multiAccountGenerateAndDeriveAddresses, generateAlias, identicon, saveAccountAndLogin, login, openAccounts, getNodeConfig
import core
import ../utils as utils
import ../types as types
import accounts/constants
import ../signals/types as signal_types
proc initNode*() =
createDir(STATUSGODIR)
createDir(KEYSTOREDIR)
discard $status_go.initKeystore(KEYSTOREDIR)

View File

@ -0,0 +1,57 @@
import # std libs
json, os, sequtils, strutils
import # vendor libs
confutils
const sep = when defined(windows): "\\" else: "/"
proc defaultDataDir(): string =
let homeDir = getHomeDir()
let parentDir =
if defined(development):
parentDir(getAppDir())
elif homeDir == "":
getCurrentDir()
elif defined(macosx):
joinPath(homeDir, "Library", "Application Support")
elif defined(windows):
let targetDir = getEnv("LOCALAPPDATA").string
if targetDir == "":
joinPath(homeDir, "AppData", "Local")
else:
targetDir
else:
let targetDir = getEnv("XDG_CONFIG_HOME").string
if targetDir == "":
joinPath(homeDir, ".config")
else:
targetDir
absolutePath(joinPath(parentDir, "DesktopNode"))
type StatusDesktopConfig = object
dataDir* {.
defaultValue: defaultDataDir()
desc: "Desktop Node data directory"
abbr: "d" .}: string
# On macOS the first time when a user gets the "App downloaded from the
# internet" warning, and clicks the Open button, the OS passes a unique process
# serial number (PSN) as -psn_... command-line argument, which we remove before
# processing the arguments with nim-confutils.
# Credit: https://github.com/bitcoin/bitcoin/blame/b6e34afe9735faf97d6be7a90fafd33ec18c0cbb/src/util/system.cpp#L383-L389
var cliParams = commandLineParams()
if defined(macosx):
cliParams.keepIf(proc(p: string): bool = not p.startsWith("-psn_"))
let desktopConfig = StatusDesktopConfig.load(cliParams)
let
baseDir = absolutePath(expandTilde(desktopConfig.dataDir))
DATADIR* = baseDir & sep
STATUSGODIR* = joinPath(baseDir, "data") & sep
KEYSTOREDIR* = joinPath(baseDir, "data", "keystore") & sep
createDir(DATADIR)

View File

@ -0,0 +1,28 @@
import json, chronicles
import status_go, ../utils
logScope:
topics = "rpc"
proc callRPC*(inputJSON: string): string =
return $status_go.callRPC(inputJSON)
proc callPrivateRPCRaw*(inputJSON: string): string =
return $status_go.callPrivateRPC(inputJSON)
proc callPrivateRPC*(methodName: string, payload = %* []): string =
try:
let inputJSON = %* {
"jsonrpc": "2.0",
"method": methodName,
"params": %payload
}
debug "callPrivateRPC", rpc_method=methodName
let response = status_go.callPrivateRPC($inputJSON)
result = $response
if parseJSON(result).hasKey("error"):
writeStackTrace()
error "rpc response error", result, payload, methodName
except Exception as e:
error "error doing rpc request", methodName = methodName, exception=e.msg

View File

@ -0,0 +1,22 @@
import
json, tables, sugar, sequtils, strutils, atomics, os
import
json_serialization, chronicles, uuids
import
./core, ../types, ../signals/types as statusgo_types, ./accounts/constants,
../utils
from status_go import nil
proc getWeb3ClientVersion*(): string =
parseJson(callPrivateRPC("web3_clientVersion"))["result"].getStr
proc startNode*(jsonConfig: string) =
echo status_go.startDesktopNode(jsonConfig)
# TODO: error handling
proc stopNode*() =
echo status_go.logout()
# TODO: error handling

16
src/status/node.nim Normal file
View File

@ -0,0 +1,16 @@
import libstatus/core as status
import ../eventemitter
type NodeModel* = ref object
events*: EventEmitter
proc newNodeModel*(): NodeModel =
result = NodeModel()
result.events = createEventEmitter()
proc delete*(self: NodeModel) =
discard
proc sendRPCMessageRaw*(self: NodeModel, msg: string): string =
echo "sending RPC message"
status.callPrivateRPCRaw(msg)

25
src/status/settings.nim Normal file
View File

@ -0,0 +1,25 @@
import json, json_serialization
import
sugar, sequtils, strutils, atomics
import libstatus/settings as libstatus_settings
import ../eventemitter
import signals/types
#TODO: temporary?
import types as LibStatusTypes
type
SettingsModel* = ref object
events*: EventEmitter
proc newSettingsModel*(events: EventEmitter): SettingsModel =
result = SettingsModel()
result.events = events
proc startNode*(self: SettingsModel, jsonConfig: string) =
libstatus_settings.startNode(jsonConfig)
proc stopNode*(self: SettingsModel) =
libstatus_settings.stopNode()

View File

@ -0,0 +1,58 @@
import NimQml, tables, json, chronicles, strutils, json_serialization
import ../types as status_types
import types, stats
import ../status
import ../../eventemitter
logScope:
topics = "signals"
QtObject:
type SignalsController* = ref object of QObject
variant*: QVariant
status*: Status
proc setup(self: SignalsController) =
self.QObject.setup
proc newController*(status: Status): SignalsController =
new(result)
result.status = status
result.variant = newQVariant(result)
result.setup()
proc delete*(self: SignalsController) =
self.variant.delete
self.QObject.delete
proc processSignal(self: SignalsController, statusSignal: string) =
var jsonSignal: JsonNode
try:
jsonSignal = statusSignal.parseJson
except:
error "Invalid signal received", data = statusSignal
return
let signalString = jsonSignal["type"].getStr
trace "Raw signal data", data = $jsonSignal
var signalType: SignalType
try:
signalType = parseEnum[SignalType](signalString)
except:
warn "Unknown signal received", type = signalString
signalType = SignalType.Unknown
return
var signal: Signal = case signalType:
of SignalType.Stats: stats.fromEvent(jsonSignal)
else: Signal()
self.status.events.emit(signalType.event, signal)
proc signalReceived*(self: SignalsController, signal: string) {.signal.}
proc receiveSignal*(self: SignalsController, signal: string) {.slot.} =
self.processSignal(signal)
self.signalReceived(signal)

View File

@ -0,0 +1,13 @@
import json
import types
proc toStats(jsonMsg: JsonNode): Stats =
result = Stats(
uploadRate: uint64(jsonMsg{"uploadRate"}.getBiggestInt()),
downloadRate: uint64(jsonMsg{"downloadRate"}.getBiggestInt())
)
proc fromEvent*(event: JsonNode): Signal =
var signal:StatsSignal = StatsSignal()
signal.stats = event["event"].toStats
result = signal

View File

@ -0,0 +1,19 @@
import json_serialization
import ../types
import ../../eventemitter
type Signal* = ref object of Args
signalType* {.serializedFieldName("type").}: SignalType
type StatusGoError* = object
error*: string
type NodeSignal* = ref object of Signal
event*: StatusGoError
type Stats* = object
uploadRate*: uint64
downloadRate*: uint64
type StatsSignal* = ref object of Signal
stats*: Stats

30
src/status/status.nim Normal file
View File

@ -0,0 +1,30 @@
import libstatus/core as libstatus_core
import types as libstatus_types
import node, settings
import libstatus/settings as libstatus_settings
import ../eventemitter
import ./tasks/task_runner_impl
export node, task_runner_impl, eventemitter
type Status* = ref object
events*: EventEmitter
node*: NodeModel
tasks*: TaskRunner
settings*: SettingsModel
proc newStatusInstance*(): Status =
result = Status()
result.tasks = newTaskRunner()
result.events = createEventEmitter()
result.settings = settings.newSettingsModel(result.events)
result.node = node.newNodeModel()
proc initNode*(self: Status) =
self.tasks.init()
proc reset*(self: Status) =
discard
proc getNodeVersion*(self: Status): string =
libstatus_settings.getWeb3ClientVersion()

View File

@ -0,0 +1,17 @@
import # vendor libs
json_serialization#, stint
export writeValue, readValue
export json_serialization
type
Task* = proc(arg: string): void {.gcsafe, nimcall.}
TaskArg* = ref object of RootObj
tptr*: ByteAddress
proc decode*[T](arg: string): T =
Json.decode(arg, T, allowUnknownFields = true)
proc encode*[T](arg: T): string =
arg.toJson(typeAnnotations = true)

View File

@ -0,0 +1,43 @@
import # std libs
strformat, tables
import # vendor libs
chronicles
import # status-desktop libs
./marathon/worker, ./marathon/common as marathon_common
export marathon_common
logScope:
topics = "marathon"
type
Marathon* = ref object
workers: Table[string, MarathonWorker]
proc start*[T: MarathonTaskArg](self: MarathonWorker, arg: T) =
self.chanSendToWorker.sendSync(arg.encode.safe)
proc newMarathon*(): Marathon =
new(result)
result.workers = initTable[string, MarathonWorker]()
proc registerWorker*(self: Marathon, worker: MarathonWorker) =
self.workers[worker.name] = worker # overwrite if exists
proc `[]`*(self: Marathon, name: string): MarathonWorker =
if not self.workers.contains(name):
raise newException(ValueError, &"""Worker '{name}' is not registered. Use 'registerWorker("{name}", {name}Worker)' to register the worker first.""")
self.workers[name]
proc init*(self: Marathon) =
for worker in self.workers.values:
worker.init()
proc teardown*(self: Marathon) =
for worker in self.workers.values:
worker.teardown()
proc onLoggedIn*(self: Marathon) =
for worker in self.workers.values:
worker.onLoggedIn()

View File

@ -0,0 +1,6 @@
import # status-desktop libs
../qt
type
MarathonTaskArg* = ref object of QObjectTaskArg
`method`*: string

View File

@ -0,0 +1,49 @@
import # std libs
json
import # vendor libs
chronicles, chronos, json_serialization, task_runner
import # status-desktop libs
../common
export
chronos, common, json_serialization
logScope:
topics = "task-marathon-worker"
type
WorkerThreadArg* = object # of RootObj
chanSendToMain*: AsyncChannel[ThreadSafeString]
chanRecvFromMain*: AsyncChannel[ThreadSafeString]
vptr*: ByteAddress
MarathonWorker* = ref object of RootObj
chanSendToWorker*: AsyncChannel[ThreadSafeString]
chanRecvFromWorker*: AsyncChannel[ThreadSafeString]
thread*: Thread[WorkerThreadArg]
vptr*: ByteAddress
method name*(self: MarathonWorker): string {.base.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method init*(self: MarathonWorker) {.base.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method teardown*(self: MarathonWorker) {.base.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method onLoggedIn*(self: MarathonWorker) {.base.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method worker(arg: WorkerThreadArg) {.async, base, gcsafe, nimcall.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")
method workerThread(arg: WorkerThreadArg) {.thread, base, gcsafe, nimcall.} =
# override this base method
raise newException(CatchableError, "Method without implementation override")

16
src/status/tasks/qt.nim Normal file
View File

@ -0,0 +1,16 @@
import # vendor libs
NimQml, json_serialization
import # status-desktop libs
./common
type
QObjectTaskArg* = ref object of TaskArg
vptr*: ByteAddress
slot*: string
proc finish*[T](arg: QObjectTaskArg, payload: T) =
signal_handler(cast[pointer](arg.vptr), Json.encode(payload), arg.slot)
proc finish*(arg: QObjectTaskArg, payload: string) =
signal_handler(cast[pointer](arg.vptr), payload, arg.slot)

View File

@ -0,0 +1,28 @@
import # vendor libs
chronicles, task_runner
import # status-desktop libs
./marathon, ./threadpool
export marathon, task_runner, threadpool
logScope:
topics = "task-runner"
type
TaskRunner* = ref object
threadpool*: ThreadPool
marathon*: Marathon
proc newTaskRunner*(): TaskRunner =
new(result)
result.threadpool = newThreadPool()
result.marathon = newMarathon()
proc init*(self: TaskRunner) =
self.threadpool.init()
self.marathon.init()
proc teardown*(self: TaskRunner) =
self.threadpool.teardown()
self.marathon.teardown()

View File

@ -0,0 +1,257 @@
import # std libs
atomics, json, sequtils, tables
import # vendor libs
chronicles, chronos, json_serialization, task_runner
import # status-desktop libs
./common
export
chronos, common, json_serialization
logScope:
topics = "task-threadpool"
type
ThreadPool* = ref object
chanRecvFromPool: AsyncChannel[ThreadSafeString]
chanSendToPool: AsyncChannel[ThreadSafeString]
thread: Thread[PoolThreadArg]
size: int
running*: Atomic[bool]
PoolThreadArg = object
chanSendToMain: AsyncChannel[ThreadSafeString]
chanRecvFromMain: AsyncChannel[ThreadSafeString]
size: int
TaskThreadArg = object
id: int
chanRecvFromPool: AsyncChannel[ThreadSafeString]
chanSendToPool: AsyncChannel[ThreadSafeString]
ThreadNotification = object
id: int
notice: string
# forward declarations
proc poolThread(arg: PoolThreadArg) {.thread.}
const MaxThreadPoolSize = 16
proc newThreadPool*(size: int = MaxThreadPoolSize): ThreadPool =
new(result)
result.chanRecvFromPool = newAsyncChannel[ThreadSafeString](-1)
result.chanSendToPool = newAsyncChannel[ThreadSafeString](-1)
result.thread = Thread[PoolThreadArg]()
result.size = size
result.running.store(false)
proc init*(self: ThreadPool) =
self.chanRecvFromPool.open()
self.chanSendToPool.open()
let arg = PoolThreadArg(
chanSendToMain: self.chanRecvFromPool,
chanRecvFromMain: self.chanSendToPool,
size: self.size
)
createThread(self.thread, poolThread, arg)
# block until we receive "ready"
discard $(self.chanRecvFromPool.recvSync())
proc teardown*(self: ThreadPool) =
self.running.store(false)
self.chanSendToPool.sendSync("shutdown".safe)
self.chanRecvFromPool.close()
self.chanSendToPool.close()
trace "[threadpool] waiting for the control thread to stop"
joinThread(self.thread)
proc start*[T: TaskArg](self: Threadpool, arg: T) =
self.chanSendToPool.sendSync(arg.encode.safe)
self.running.store(true)
proc runner(arg: TaskThreadArg) {.async.} =
arg.chanRecvFromPool.open()
arg.chanSendToPool.open()
let noticeToPool = ThreadNotification(id: arg.id, notice: "ready")
trace "[threadpool task thread] sending 'ready'", threadid=arg.id
await arg.chanSendToPool.send(noticeToPool.encode.safe)
while true:
trace "[threadpool task thread] waiting for message"
let received = $(await arg.chanRecvFromPool.recv())
if received == "shutdown":
trace "[threadpool task thread] received 'shutdown'"
break
let
parsed = parseJson(received)
messageType = parsed{"$type"}.getStr
trace "[threadpool task thread] initiating task", messageType=messageType,
threadid=arg.id
try:
let task = cast[Task](parsed{"tptr"}.getInt)
try:
task(received)
except Exception as e:
error "[threadpool task thread] exception", error=e.msg
except Exception as e:
error "[threadpool task thread] unknown message", message=received
let noticeToPool = ThreadNotification(id: arg.id, notice: "done")
trace "[threadpool task thread] sending 'done' notice to pool",
threadid=arg.id
await arg.chanSendToPool.send(noticeToPool.encode.safe)
arg.chanRecvFromPool.close()
arg.chanSendToPool.close()
proc taskThread(arg: TaskThreadArg) {.thread.} =
waitFor runner(arg)
proc pool(arg: PoolThreadArg) {.async.} =
let
chanSendToMain = arg.chanSendToMain
chanRecvFromMainOrTask = arg.chanRecvFromMain
var threadsBusy = newTable[int, tuple[thr: Thread[TaskThreadArg],
chanSendToTask: AsyncChannel[ThreadSafeString]]]()
var threadsIdle = newSeq[tuple[id: int, thr: Thread[TaskThreadArg],
chanSendToTask: AsyncChannel[ThreadSafeString]]](arg.size)
var taskQueue: seq[string] = @[] # FIFO queue
var allReady = 0
chanSendToMain.open()
chanRecvFromMainOrTask.open()
trace "[threadpool] sending 'ready' to main thread"
await chanSendToMain.send("ready".safe)
for i in 0..<arg.size:
let id = i + 1
let chanSendToTask = newAsyncChannel[ThreadSafeString](-1)
chanSendToTask.open()
trace "[threadpool] adding to threadsIdle", threadid=id
threadsIdle[i].id = id
createThread(
threadsIdle[i].thr,
taskThread,
TaskThreadArg(id: id, chanRecvFromPool: chanSendToTask,
chanSendToPool: chanRecvFromMainOrTask
)
)
threadsIdle[i].chanSendToTask = chanSendToTask
# when task received and number of busy threads == MaxThreadPoolSize,
# then put the task in a queue
# when task received and number of busy threads < MaxThreadPoolSize, pop
# a thread from threadsIdle, track that thread in threadsBusy, and run
# task in that thread
# if "done" received from a thread, remove thread from threadsBusy, and
# push thread into threadsIdle
while true:
trace "[threadpool] waiting for message"
var task = $(await chanRecvFromMainOrTask.recv())
if task == "shutdown":
trace "[threadpool] sending 'shutdown' to all task threads"
for tpl in threadsIdle:
await tpl.chanSendToTask.send("shutdown".safe)
for tpl in threadsBusy.values:
await tpl.chanSendToTask.send("shutdown".safe)
break
let
jsonNode = parseJson(task)
messageType = jsonNode{"$type"}.getStr
trace "[threadpool] determined message type", messageType=messageType
case messageType
of "ThreadNotification":
try:
let notification = decode[ThreadNotification](task)
trace "[threadpool] received notification",
notice=notification.notice, threadid=notification.id
if notification.notice == "ready":
trace "[threadpool] received 'ready' from a task thread"
allReady = allReady + 1
elif notification.notice == "done":
let tpl = threadsBusy[notification.id]
trace "[threadpool] adding to threadsIdle",
newlength=(threadsIdle.len + 1)
threadsIdle.add (notification.id, tpl.thr, tpl.chanSendToTask)
trace "[threadpool] removing from threadsBusy",
newlength=(threadsBusy.len - 1), threadid=notification.id
threadsBusy.del notification.id
if taskQueue.len > 0:
trace "[threadpool] removing from taskQueue",
newlength=(taskQueue.len - 1)
task = taskQueue[0]
taskQueue.delete 0, 0
trace "[threadpool] removing from threadsIdle",
newlength=(threadsIdle.len - 1)
let tpl = threadsIdle[0]
threadsIdle.delete 0, 0
trace "[threadpool] adding to threadsBusy",
newlength=(threadsBusy.len + 1), threadid=tpl.id
threadsBusy.add tpl.id, (tpl.thr, tpl.chanSendToTask)
await tpl.chanSendToTask.send(task.safe)
else:
error "[threadpool] unknown notification", notice=notification.notice
except Exception as e:
warn "[threadpool] unknown error in thread notification", message=task, error=e.msg
else: # must be a request to do task work
if allReady < arg.size or threadsBusy.len == arg.size:
# add to queue
trace "[threadpool] adding to taskQueue",
newlength=(taskQueue.len + 1)
taskQueue.add task
# do we have available threads in the threadpool?
elif threadsBusy.len < arg.size:
# check if we have tasks waiting on queue
if taskQueue.len > 0:
# remove first element from the task queue
trace "[threadpool] adding to taskQueue",
newlength=(taskQueue.len + 1)
taskQueue.add task
trace "[threadpool] removing from taskQueue",
newlength=(taskQueue.len - 1)
task = taskQueue[0]
taskQueue.delete 0, 0
trace "[threadpool] removing from threadsIdle",
newlength=(threadsIdle.len - 1)
let tpl = threadsIdle[0]
threadsIdle.delete 0, 0
trace "[threadpool] adding to threadsBusy",
newlength=(threadsBusy.len + 1), threadid=tpl.id
threadsBusy.add tpl.id, (tpl.thr, tpl.chanSendToTask)
await tpl.chanSendToTask.send(task.safe)
var allTaskThreads: seq[Thread[TaskThreadArg]] = @[]
for tpl in threadsIdle:
tpl.chanSendToTask.close()
allTaskThreads.add tpl.thr
for tpl in threadsBusy.values:
tpl.chanSendToTask.close()
allTaskThreads.add tpl.thr
chanSendToMain.close()
chanRecvFromMainOrTask.close()
trace "[threadpool] waiting for all task threads to stop"
joinThreads(allTaskThreads)
proc poolThread(arg: PoolThreadArg) {.thread.} =
waitFor pool(arg)

70
src/status/types.nim Normal file
View File

@ -0,0 +1,70 @@
import json, options, typetraits, tables, sequtils, strutils
import json_serialization, stint
import libstatus/accounts/constants
import ../eventemitter
type SignalType* {.pure.} = enum
Message = "messages.new"
Wallet = "wallet"
NodeReady = "node.ready"
NodeCrashed = "node.crashed"
NodeStarted = "node.started"
NodeStopped = "node.stopped"
NodeLogin = "node.login"
EnvelopeSent = "envelope.sent"
EnvelopeExpired = "envelope.expired"
MailserverRequestCompleted = "mailserver.request.completed"
MailserverRequestExpired = "mailserver.request.expired"
DiscoveryStarted = "discovery.started"
DiscoveryStopped = "discovery.stopped"
DiscoverySummary = "discovery.summary"
SubscriptionsData = "subscriptions.data"
SubscriptionsError = "subscriptions.error"
WhisperFilterAdded = "whisper.filter.added"
CommunityFound = "community.found"
Stats = "stats"
Unknown
proc event*(self:SignalType):string =
result = "signal:" & $self
type RpcError* = ref object
code*: int
message*: string
type
RpcResponse* = ref object
jsonrpc*: string
result*: string
id*: int
error*: RpcError
# TODO: replace all RpcResponse and RpcResponseTyped occurances with a generic
# form of RpcReponse. IOW, rename RpceResponseTyped*[T] to RpcResponse*[T] and
# remove RpcResponse.
RpcResponseTyped*[T] = object
jsonrpc*: string
result*: T
id*: int
error*: RpcError
type
StatusGoException* = object of CatchableError
type
RpcException* = object of CatchableError
proc `%`*(stuint256: Stuint[256]): JsonNode =
newJString($stuint256)
proc readValue*(reader: var JsonReader, value: var Stuint[256])
{.raises: [IOError, SerializationError, Defect].} =
try:
let strVal = reader.readValue(string)
value = strVal.parse(Stuint[256])
except:
try:
let intVal = reader.readValue(int)
value = intVal.stuint(256)
except:
raise newException(SerializationError, "Expected string or int representation of Stuint[256]")

112
src/status/utils.nim Normal file
View File

@ -0,0 +1,112 @@
import json, random, strutils, strformat, tables, chronicles, unicode, times
from sugar import `=>`, `->`
import stint
from times import getTime, toUnix, nanosecond
proc prefix*(methodName: string, isExt:bool = true): string =
result = "waku"
result = result & (if isExt: "ext_" else: "_")
result = result & methodName
proc handleRPCErrors*(response: string) =
let parsedReponse = parseJson(response)
if (parsedReponse.hasKey("error")):
raise newException(ValueError, parsedReponse["error"]["message"].str)
proc toStUInt*[bits: static[int]](flt: float, T: typedesc[StUint[bits]]): T =
var stringValue = fmt"{flt:<.0f}"
stringValue.removeSuffix('.')
if (flt >= 0):
result = parse($stringValue, StUint[bits])
else:
result = parse("0", StUint[bits])
proc toUInt256*(flt: float): UInt256 =
toStUInt(flt, StUInt[256])
proc toUInt64*(flt: float): StUInt[64] =
toStUInt(flt, StUInt[64])
proc eth2Wei*(eth: float, decimals: int = 18): UInt256 =
let weiValue = eth * parseFloat(alignLeft("1", decimals + 1, '0'))
weiValue.toUInt256
proc gwei2Wei*(gwei: float): UInt256 =
eth2Wei(gwei, 9)
proc wei2Eth*(input: Stuint[256], decimals: int = 18): string =
var one_eth = u256(10).pow(decimals) # fromHex(Stuint[256], "DE0B6B3A7640000")
var (eth, remainder) = divmod(input, one_eth)
let leading_zeros = "0".repeat(($one_eth).len - ($remainder).len - 1)
fmt"{eth}.{leading_zeros}{remainder}"
proc wei2Eth*(input: string, decimals: int): string =
try:
var input256: Stuint[256]
if input.contains("e+"): # we have a js string BN, ie 1e+21
let
inputSplit = input.split("e+")
whole = inputSplit[0].u256
remainder = u256(10).pow(inputSplit[1].parseInt)
input256 = whole * remainder
else:
input256 = input.u256
result = wei2Eth(input256, decimals)
except Exception as e:
error "Error parsing this wei value", input, msg=e.msg
result = "0"
proc first*(jArray: JsonNode, fieldName, id: string): JsonNode =
if jArray == nil:
return nil
if jArray.kind != JArray:
raise newException(ValueError, "Parameter 'jArray' is a " & $jArray.kind & ", but must be a JArray")
for child in jArray.getElems:
if child{fieldName}.getStr.toLower == id.toLower:
return child
proc any*(jArray: JsonNode, fieldName, id: string): bool =
if jArray == nil:
return false
result = false
for child in jArray.getElems:
if child{fieldName}.getStr.toLower == id.toLower:
return true
proc isEmpty*(a: JsonNode): bool =
case a.kind:
of JObject: return a.fields.len == 0
of JArray: return a.elems.len == 0
of JString: return a.str == ""
of JNull: return true
else:
return false
proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}): T {.inline.} =
let results = s.filter(pred)
if results.len == 0:
return default(type(T))
result = results[0]
proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}, found: var bool): T {.inline.} =
let results = s.filter(pred)
if results.len == 0:
found = false
return default(type(T))
result = results[0]
found = true
proc isUnique*[T](key: T, existingKeys: var seq[T]): bool =
# If the key doesn't exist in the existingKeys seq, add it and return true.
# Otherwise, the key already existed, so return false.
# Can be used to deduplicate sequences with `deduplicate[T]`.
if not existingKeys.contains(key):
existingKeys.add key
return true
return false
proc deduplicate*[T](txs: var seq[T], key: (T) -> string) =
var existingKeys: seq[string] = @[]
txs.keepIf(tx => tx.key().isUnique(existingKeys))

View File

@ -1,12 +1,9 @@
import NimQml, chronicles, os, strformat
#import app/node/core as node
#import app/utilsView/core as utilsView
#import status/signals/core as signals
#import status/types
#import status/constants
import app/node/core as node
import status/signals/core as signals
import status_go
#import status/status as statuslib
import status/status as statuslib
import ./eventemitter
var signalsQObjPointer: pointer
@ -28,9 +25,9 @@ proc mainProc() =
let
fleetConfig = readFile(joinPath(getAppDir(), fleets))
# status = statuslib.newStatusInstance(fleetConfig)
status = statuslib.newStatusInstance()
#status.initNode()
status.initNode()
enableHDPI()
initializeOpenGL()
@ -85,23 +82,19 @@ proc mainProc() =
app.installEventFilter(dockShowAppEvent)
app.installEventFilter(osThemeEvent)
# let signalController = signals.newController(status)
#defer:
# signalsQObjPointer = nil
#signalController.delete()
let signalController = signals.newController(status)
defer:
signalsQObjPointer = nil
signalController.delete()
# We need this global variable in order to be able to access the application
# from the non-closure callback passed to `libstatus.setSignalEventCallback`
#signalsQObjPointer = cast[pointer](signalController.vptr)
signalsQObjPointer = cast[pointer](signalController.vptr)
# var node = node.newController(status, netAccMgr)
#defer: node.delete()
#engine.setRootContextProperty("nodeModel", node.variant)
#var utilsController = utilsView.newController(status)
#defer: utilsController.delete()
#engine.setRootContextProperty("utilsModel", utilsController.variant)
var node = node.newController(status, fleetConfig)
defer: node.delete()
engine.setRootContextProperty("nodeModel", node.variant)
node.init()
proc changeLanguage(locale: string) =
if (locale == currentLanguageCode):
@ -110,29 +103,12 @@ proc mainProc() =
let shouldRetranslate = not defined(linux)
engine.setTranslationPackage(joinPath(i18nPath, fmt"qml_{locale}.qm"), shouldRetranslate)
# status.tasks.marathon.onLoggedIn()
# this should be the last defer in the scope
defer:
info "Status app is shutting down..."
#status.tasks.teardown()
status.tasks.teardown()
#initControllers()
# Handle node.stopped signal when user has logged out
# status.events.once("nodeStopped") do(a: Args):
# TODO: remove this once accounts are not tracked in the AccountsModel
# status.reset()
# 2. Re-init controllers that don't require a running node
# initControllers()
# engine.setRootContextProperty("signals", signalController.variant)
engine.setRootContextProperty("signals", signalController.variant)
var prValue = newQVariant(if defined(production): true else: false)
engine.setRootContextProperty("production", prValue)
@ -148,9 +124,8 @@ proc mainProc() =
# it will be passed as a regular C function to libstatus. This means that
# we cannot capture any local variables here (we must rely on globals)
var callback: SignalCallback = proc(p0: cstring) {.cdecl.} =
discard
# if signalsQObjPointer != nil:
#signal_handler(signalsQObjPointer, p0, "receiveSignal")
if signalsQObjPointer != nil:
signal_handler(signalsQObjPointer, p0, "receiveSignal")
status_go.setSignalEventCallback(callback)

215
ui/app/AppLayout.qml Normal file
View File

@ -0,0 +1,215 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import Qt.labs.platform 1.1
import QtQml.StateMachine 1.14 as DSM
import Qt.labs.settings 1.0
import QtQuick.Window 2.12
import QtQml 2.13
import QtQuick.Window 2.0
import QtQuick.Controls.Universal 2.12
import DotherSide 0.1
import "../shared"
import "../shared/status"
import "../imports"
Column {
id: generalColumn
FleetsModal {
id: fleetModal
}
StatusSectionHeadline {
//% "Bloom filter level"
text: qsTrId("bloom-filter-level")
topPadding: Style.current.bigPadding
bottomPadding: Style.current.padding
}
Row {
spacing: 11
ButtonGroup {
id: bloomGroup
}
BloomSelectorButton {
id: btnBloomLight
buttonGroup: bloomGroup
enabled: !nodeModel.nodeActive
checkedByDefault: appSettings.bloomLevel == "light"
//% "Light Node"
btnText: qsTrId("light-node")
onToggled: {
if (appSettings.bloomLevel != "light") {
appSettings.bloomLevel = "light";
} else {
btnBloomLight.click()
}
}
}
BloomSelectorButton {
id: btnBloomNormal
enabled: !nodeModel.nodeActive
buttonGroup: bloomGroup
checkedByDefault: appSettings.bloomLevel == "normal"
//% "Normal"
btnText: qsTrId("normal")
onToggled: {
if (appSettings.bloomLevel != "normal") {
appSettings.bloomLevel = "normal";
} else {
btnBloomNormal.click()
}
}
}
BloomSelectorButton {
id: btnBloomFull
enabled: !nodeModel.nodeActive
buttonGroup: bloomGroup
checkedByDefault: appSettings.bloomLevel == "full"
//% "Full Node"
btnText: qsTrId("full-node")
onToggled: {
if (appSettings.bloomLevel != "full") {
appSettings.bloomLevel = "full";
} else {
btnBloomFull.click()
}
}
}
}
Rate {}
StatusSettingsLineButton {
//% "Fleet"
text: qsTrId("fleet")
currentValue: appSettings.fleet
isEnabled: !nodeModel.nodeActive
onClicked: {
fleetModal.open()
}
}
Connections {
target: nodeModel
onNodeActiveChanged: {
startNodeBtn.enabled = true
}
}
StatusSettingsLineButton {
id: startNodeBtn
text: qsTr("Start Node")
isSwitch: true
switchChecked: nodeModel.nodeActive
onClicked: {
enabled = false
if(switchChecked){
nodeModel.stopNode();
return
}
let configJSON = {
"EnableNTPSync": true,
"KeyStoreDir": appSettings.dataDir + "/keystore",
"NetworkId": appSettings.networkId,
"LogEnabled": appSettings.LogEnabled,
"LogFile": appSettings.LogFile,
"LogLevel": appSettings.logLevel,
"ListenAddr": "0.0.0.0:30303", // TODO: Add setting
"HTTPEnabled": true, // TODO: Add setting
"HTTPHost": "0.0.0.0", // TODO: Add setting
"DataDir": appSettings.dataDir,
"HTTPPort": 8545, // TODO: Add setting
"APIModules": "eth,web3,admin", // TODO: Add setting
"RegisterTopics": ["whispermail"],
"NodeKey": appSettings.nodeKey,
"WakuConfig": {
"Enabled": !appSettings.useWakuV2,
"DataDir": "./waku",
"BloomFilterMode": appSettings.bloomLevel == "normal",
"LightClient": false,
"MinimumPoW": 0.001,
"FullNode": appSettings.bloomLevel == "full"
},
"WakuV2Config": {
"Enabled": appSettings.useWakuV2,
"Host": "0.0.0.0", // TODO: Add setting
"Port": 0 // TODO: Add setting
},
"RequireTopics": {
"whisper": {
"Max": 2,
"Min": 2
}
},
"NoDiscovery": false,
"Rendezvous": false,
"ClusterConfig": {
"Enabled": true,
"Fleet": appSettings.fleet,
"RendezvousNodes": [],
"BootNodes": [
// TODO: Add setting
"enode://6e6554fb3034b211398fcd0f0082cbb6bd13619e1a7e76ba66e1809aaa0c5f1ac53c9ae79cf2fd4a7bacb10d12010899b370c75fed19b991d9c0cdd02891abad@47.75.99.169:443",
"enode://436cc6f674928fdc9a9f7990f2944002b685d1c37f025c1be425185b5b1f0900feaf1ccc2a6130268f9901be4a7d252f37302c8335a2c1a62736e9232691cc3a@178.128.138.128:443",
"enode://32ff6d88760b0947a3dee54ceff4d8d7f0b4c023c6dad34568615fcae89e26cc2753f28f12485a4116c977be937a72665116596265aa0736b53d46b27446296a@34.70.75.208:443",
"enode://23d0740b11919358625d79d4cac7d50a34d79e9c69e16831c5c70573757a1f5d7d884510bc595d7ee4da3c1508adf87bbc9e9260d804ef03f8c1e37f2fb2fc69@47.52.106.107:443",
"enode://5395aab7833f1ecb671b59bf0521cf20224fe8162fc3d2675de4ee4d5636a75ec32d13268fc184df8d1ddfa803943906882da62a4df42d4fccf6d17808156a87@178.128.140.188:443",
"enode://5405c509df683c962e7c9470b251bb679dd6978f82d5b469f1f6c64d11d50fbd5dd9f7801c6ad51f3b20a5f6c7ffe248cc9ab223f8bcbaeaf14bb1c0ef295fd0@35.223.215.156:443",
"enode://b957e51f41e4abab8382e1ea7229e88c6e18f34672694c6eae389eac22dab8655622bbd4a08192c321416b9becffaab11c8e2b7a5d0813b922aa128b82990dab@47.75.222.178:443",
"enode://66ba15600cda86009689354c3a77bdf1a97f4f4fb3ab50ffe34dbc904fac561040496828397be18d9744c75881ffc6ac53729ddbd2cdbdadc5f45c400e2622f7@178.128.141.87:443",
"enode://182ed5d658d1a1a4382c9e9f7c9e5d8d9fec9db4c71ae346b9e23e1a589116aeffb3342299bdd00e0ab98dbf804f7b2d8ae564ed18da9f45650b444aed79d509@34.68.132.118:443",
"enode://8bebe73ddf7cf09e77602c7d04c93a73f455b51f24ae0d572917a4792f1dec0bb4c562759b8830cc3615a658d38c1a4a38597a1d7ae3ba35111479fc42d65dec@47.75.85.212:443",
"enode://4ea35352702027984a13274f241a56a47854a7fd4b3ba674a596cff917d3c825506431cf149f9f2312a293bb7c2b1cca55db742027090916d01529fe0729643b@134.209.136.79:443",
"enode://fbeddac99d396b91d59f2c63a3cb5fc7e0f8a9f7ce6fe5f2eed5e787a0154161b7173a6a73124a4275ef338b8966dc70a611e9ae2192f0f2340395661fad81c0@34.67.230.193:443",
"enode://ac3948b2c0786ada7d17b80cf869cf59b1909ea3accd45944aae35bf864cc069126da8b82dfef4ddf23f1d6d6b44b1565c4cf81c8b98022253c6aea1a89d3ce2@47.75.88.12:443",
"enode://ce559a37a9c344d7109bd4907802dd690008381d51f658c43056ec36ac043338bd92f1ac6043e645b64953b06f27202d679756a9c7cf62fdefa01b2e6ac5098e@134.209.136.123:443",
"enode://c07aa0deea3b7056c5d45a85bca42f0d8d3b1404eeb9577610f386e0a4744a0e7b2845ae328efc4aa4b28075af838b59b5b3985bffddeec0090b3b7669abc1f3@35.226.92.155:443",
"enode://385579fc5b14e04d5b04af7eee835d426d3d40ccf11f99dbd95340405f37cf3bbbf830b3eb8f70924be0c2909790120682c9c3e791646e2d5413e7801545d353@47.244.221.249:443",
"enode://4e0a8db9b73403c9339a2077e911851750fc955db1fc1e09f81a4a56725946884dd5e4d11258eac961f9078a393c45bcab78dd0e3bc74e37ce773b3471d2e29c@134.209.136.101:443",
"enode://0624b4a90063923c5cc27d12624b6a49a86dfb3623fcb106801217fdbab95f7617b83fa2468b9ae3de593ff6c1cf556ccf9bc705bfae9cb4625999765127b423@35.222.158.246:443",
"enode://b77bffc29e2592f30180311dd81204ab845e5f78953b5ba0587c6631be9c0862963dea5eb64c90617cf0efd75308e22a42e30bc4eb3cd1bbddbd1da38ff6483e@47.75.10.177:443",
"enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@178.128.141.249:443",
"enode://a5fe9c82ad1ffb16ae60cb5d4ffe746b9de4c5fbf20911992b7dd651b1c08ba17dd2c0b27ee6b03162c52d92f219961cc3eb14286aca8a90b75cf425826c3bd8@104.154.230.58:443",
"enode://cf5f7a7e64e3b306d1bc16073fba45be3344cb6695b0b616ccc2da66ea35b9f35b3b231c6cf335fdfaba523519659a440752fc2e061d1e5bc4ef33864aac2f19@47.75.221.196:443",
"enode://887cbd92d95afc2c5f1e227356314a53d3d18855880ac0509e0c0870362aee03939d4074e6ad31365915af41d34320b5094bfcc12a67c381788cd7298d06c875@178.128.141.0:443",
"enode://282e009967f9f132a5c2dd366a76319f0d22d60d0c51f7e99795a1e40f213c2705a2c10e4cc6f3890319f59da1a535b8835ed9b9c4b57c3aad342bf312fd7379@35.223.240.17:443",
"enode://13d63a1f85ccdcbd2fb6861b9bd9d03f94bdba973608951f7c36e5df5114c91de2b8194d71288f24bfd17908c48468e89dd8f0fb8ccc2b2dedae84acdf65f62a@47.244.210.80:443",
"enode://2b01955d7e11e29dce07343b456e4e96c081760022d1652b1c4b641eaf320e3747871870fa682e9e9cfb85b819ce94ed2fee1ac458904d54fd0b97d33ba2c4a4@134.209.136.112:443",
"enode://b706a60572634760f18a27dd407b2b3582f7e065110dae10e3998498f1ae3f29ba04db198460d83ed6d2bfb254bb06b29aab3c91415d75d3b869cd0037f3853c@35.239.5.162:443",
"enode://32915c8841faaef21a6b75ab6ed7c2b6f0790eb177ad0f4ea6d731bacc19b938624d220d937ebd95e0f6596b7232bbb672905ee12601747a12ee71a15bfdf31c@47.75.59.11:443",
"enode://0d9d65fcd5592df33ed4507ce862b9c748b6dbd1ea3a1deb94e3750052760b4850aa527265bbaf357021d64d5cc53c02b410458e732fafc5b53f257944247760@178.128.141.42:443",
"enode://e87f1d8093d304c3a9d6f1165b85d6b374f1c0cc907d39c0879eb67f0a39d779be7a85cbd52920b6f53a94da43099c58837034afa6a7be4b099bfcd79ad13999@35.238.106.101:443"
],
"TrustedMailServers": [
// TODO: Add setting
"enode://606ae04a71e5db868a722c77a21c8244ae38f1bd6e81687cc6cfe88a3063fa1c245692232f64f45bd5408fed5133eab8ed78049332b04f9c110eac7f71c1b429@47.75.247.214:443",
"enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@178.128.142.54:443",
"enode://ee2b53b0ace9692167a410514bca3024695dbf0e1a68e1dff9716da620efb195f04a4b9e873fb9b74ac84de801106c465b8e2b6c4f0d93b8749d1578bfcaf03e@104.197.238.144:443",
"enode://2c8de3cbb27a3d30cbb5b3e003bc722b126f5aef82e2052aaef032ca94e0c7ad219e533ba88c70585ebd802de206693255335b100307645ab5170e88620d2a81@47.244.221.14:443",
"enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@178.128.142.26:443",
"enode://30211cbd81c25f07b03a0196d56e6ce4604bb13db773ff1c0ea2253547fafd6c06eae6ad3533e2ba39d59564cfbdbb5e2ce7c137a5ebb85e99dcfc7a75f99f55@23.236.58.92:443",
"enode://e85f1d4209f2f99da801af18db8716e584a28ad0bdc47fbdcd8f26af74dbd97fc279144680553ec7cd9092afe683ddea1e0f9fc571ebcb4b1d857c03a088853d@47.244.129.82:443",
"enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@178.128.142.94:443",
"enode://44160e22e8b42bd32a06c1532165fa9e096eebedd7fa6d6e5f8bbef0440bc4a4591fe3651be68193a7ec029021cdb496cfe1d7f9f1dc69eb99226e6f39a7a5d4@35.225.221.245:443"
],
"PushNotificationsServers": [],
"StaticNodes": [
// TODO: Add setting
"enode://b77bffc29e2592f30180311dd81204ab845e5f78953b5ba0587c6631be9c0862963dea5eb64c90617cf0efd75308e22a42e30bc4eb3cd1bbddbd1da38ff6483e@47.75.10.177:443",
"enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@178.128.141.249:443"
]
}
}
nodeModel.startNode(JSON.stringify(configJSON))
}
}
}

View File

@ -0,0 +1,70 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
import "../shared"
import "../shared/status"
Rectangle {
property var buttonGroup
//% "TODO"
property string btnText: qsTrId("todo")
property bool hovered: false
property bool checkedByDefault: false
property bool enabled: true
signal checked()
signal toggled(bool checked)
function click(){
radioBtn.toggle()
}
id: root
border.color: hovered || radioBtn.checked ? (enabled ? Style.current.primary : Style.current.border ): Style.current.border
border.width: 1
color: Style.current.transparent
width: 150
height: 100
clip: true
radius: Style.current.radius
StatusRadioButton {
id: radioBtn
ButtonGroup.group: buttonGroup
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 14
enabled: root.enabled
checked: root.checkedByDefault
onCheckedChanged: {
if (checked) {
root.checked()
}
}
}
StyledText {
id: txt
text: btnText
font.pixelSize: 15
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: radioBtn.bottom
anchors.topMargin: 6
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: root.hovered = true
onExited: root.hovered = false
onClicked: {
if (!root.enabled) return;
radioBtn.toggle()
root.toggled(radioBtn.checked)
}
}
}

View File

@ -0,0 +1,38 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
import "../shared"
import "../shared/status"
StatusRadioButtonRow {
property string fleetName: ""
property string newFleet: ""
text: fleetName
buttonGroup: fleetSettings
checked: appSettings.fleet === text
onRadioCheckedChanged: {
if (checked) {
if (appSettings.fleet === fleetName) return;
newFleet = fleetName;
openPopup(confirmDialogComponent)
}
}
Component {
id: confirmDialogComponent
ConfirmationDialog {
//% "Warning!"
title: qsTrId("close-app-title")
//% "Change fleet to %1"
confirmationText: qsTrId("change-fleet-to--1").arg(newFleet)
onConfirmButtonClicked: {
appSettings.fleet = newFleet
}
onClosed: {
destroy();
}
}
}
}

49
ui/app/FleetsModal.qml Normal file
View File

@ -0,0 +1,49 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
import "../shared"
import "../shared/status"
ModalPopup {
id: popup
//% "Fleet"
title: qsTrId("fleet")
property string newFleet: "";
height: 340
Column {
id: column
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.leftMargin: Style.current.padding
spacing: 0
ButtonGroup { id: fleetSettings }
FleetRadioSelector {
fleetName: Constants.eth_prod
}
FleetRadioSelector {
fleetName: Constants.eth_staging
}
FleetRadioSelector {
fleetName: Constants.eth_test
}
FleetRadioSelector {
fleetName: Constants.waku_prod
}
FleetRadioSelector {
fleetName: Constants.waku_test
}
}
}

78
ui/app/Rate.qml Normal file
View File

@ -0,0 +1,78 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
import "../shared"
import "../shared/status"
Column {
spacing: 0
StatusSectionHeadline {
text: qsTr("Bandwidth")
topPadding: Style.current.bigPadding
bottomPadding: Style.current.padding
}
Row {
width: parent.width
spacing: 10
StyledText {
text: qsTr("Upload")
width: 80
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: 140
height: 44
Input {
id: uploadRate
text: Math.round(parseInt(nodeModel.uploadRate, 10) / 1024 * 100) / 100
width: parent.width
readOnly: true
customHeight: 44
placeholderText: "..."
anchors.top: parent.top
}
StyledText {
color: Style.current.secondaryText
text: qsTr("Kb/s")
anchors.verticalCenter: parent.verticalCenter
anchors.right: uploadRate.right
anchors.rightMargin: Style.current.padding
font.pixelSize: 15
}
}
StyledText {
text: qsTr("Download")
width: 80
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: 140
height: 44
Input {
id: downloadRate
text: Math.round(parseInt(nodeModel.downloadRate, 10) / 1024 * 100) / 100
width: parent.width
readOnly: true
customHeight: 44
placeholderText: "..."
anchors.top: parent.top
}
StyledText {
color: Style.current.secondaryText
text: qsTr("Kb/s")
anchors.verticalCenter: parent.verticalCenter
anchors.right: downloadRate.right
anchors.rightMargin: Style.current.padding
font.pixelSize: 15
}
}
}
}

3
ui/app/img/caret.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.46967 0.46967C0.762563 0.176777 1.23744 0.176777 1.53033 0.46967L5.64645 4.58579C5.84171 4.78105 6.15829 4.78105 6.35355 4.58579L10.4697 0.46967C10.7626 0.176777 11.2374 0.176777 11.5303 0.46967C11.8232 0.762563 11.8232 1.23744 11.5303 1.53033L6.88388 6.17678C6.39573 6.66493 5.60427 6.66493 5.11612 6.17678L0.46967 1.53033C0.176777 1.23744 0.176777 0.762563 0.46967 0.46967Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

View File

@ -1,7 +0,0 @@
pragma Singleton
import QtQuick 2.13
QtObject {
property int currentMenuTab: 0
}

4
ui/imports/qmldir Normal file
View File

@ -0,0 +1,4 @@
module Style
singleton Style 1.0 ./Style.qml
singleton Constants 1.0 ./Constants.qml
singleton Utils 1.0 ./Utils.qml

View File

@ -12,36 +12,59 @@ import QtQuick.Controls.Universal 2.12
import DotherSide 0.1
import "./shared"
import "./shared/status"
import "./imports"
import "./app"
StatusWindow {
property bool popupOpened: false
function openPopup(popupComponent, params = {}) {
const popup = popupComponent.createObject(applicationWindow, params);
popup.open()
}
Universal.theme: Universal.System
function genHexString(len) {
const hex = '0123456789ABCDEF';
let output = '';
for (let i = 0; i < len; ++i) {
output += hex.charAt(Math.floor(Math.random() * hex.length));
}
return output;
}
Settings {
id: globalSettings
category: "global"
fileName: profileModel.settings.globalSettingsFile
id: appSettings
fileName: nodeModel.dataDir + "/qt/settings"
property string locale: "en"
property int theme: 2
Component.onCompleted: {
profileModel.changeLocale(locale)
}
property int networkId: 1
property bool logEnabled: true
property string logFile: "geth.log"
property string logLevel: "INFO"
property string dataDir: nodeModel.dataDir
property string fleet: Constants.eth_prod
property string nodeKey: genHexString(64)
property string bloomLevel: "full"
property bool useWakuV2: false
}
id: applicationWindow
objectName: "mainWindow"
minimumWidth: 900
minimumHeight: 600
width: 1232
height: 770
width: 500
height: 400
minimumWidth: width
minimumHeight: height
maximumWidth: width
maximumHeight: height
color: Style.current.background
title: {
// Set application settings
//% "Status Desktop"
Qt.application.name = qsTrId("status-desktop")
Qt.application.name = "Status Node"
Qt.application.organization = "Status"
Qt.application.domain = "status.im"
return Qt.application.name
@ -52,18 +75,8 @@ StatusWindow {
Connections {
target: applicationWindow
onClosing: {
/*if (loader.sourceComponent == login) {
applicationWindow.visible = false
close.accepted = false
}
else if (loader.sourceComponent == app) {
if (loader.item.appSettings.quitOnClose) {
close.accepted = true
} else {
applicationWindow.visible = false
close.accepted = false
}
}*/
applicationWindow.visible = false
close.accepted = false
}
onActiveChanged: {
@ -86,11 +99,11 @@ StatusWindow {
}
function changeThemeFromOutside() {
Style.changeTheme(globalSettings.theme, systemPalette.isCurrentSystemThemeDark())
Style.changeTheme(appSettings.theme, systemPalette.isCurrentSystemThemeDark())
}
Component.onCompleted: {
Style.changeTheme(globalSettings.theme, systemPalette.isCurrentSystemThemeDark())
Style.changeTheme(appSettings.theme, systemPalette.isCurrentSystemThemeDark())
setX(Qt.application.screens[0].width / 2 - width / 2);
setY(Qt.application.screens[0].height / 2 - height / 2);
@ -149,11 +162,12 @@ StatusWindow {
}
}
Loader {
id: loader
anchors.fill: parent
property var appSettings
AppLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
}
MacTrafficLights {
@ -164,25 +178,12 @@ StatusWindow {
visible: Qt.platform.os === "osx" && !applicationWindow.isFullScreen
onClose: {
if (loader.sourceComponent == login) {
Qt.quit();
}
else if (loader.sourceComponent == app) {
if (loader.item.appSettings.quitOnClose) {
Qt.quit();
} else {
applicationWindow.visible = false;
}
}
applicationWindow.visible = false;
}
onMinimised: {
applicationWindow.showMinimized()
}
onMaximized: {
applicationWindow.toggleFullScreen()
}
}
}

View File

@ -0,0 +1,101 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
import "../shared/status"
import "./"
ModalPopup {
id: confirmationDialog
property Popup parentPopup
property string btnType: "warn"
property bool showCancelButton: false
property alias checkbox: checkbox
height: 186
width: 400
//% "Confirm your action"
title: qsTrId("confirm-your-action")
//% "Confirm"
property string confirmButtonLabel: qsTrId("close-app-button")
//% "Cancel"
//% "Cancel"
property string cancelButtonLabel: qsTrId("browsing-cancel")
//% "Are you sure you want to this?"
property string confirmationText: qsTrId("are-you-sure-you-want-to-this-")
property var value
signal confirmButtonClicked()
signal cancelButtonClicked()
property var executeConfirm
property var executeCancel
Item {
anchors.fill: parent
StyledText {
id: innerText
text: confirmationDialog.confirmationText
font.pixelSize: 15
anchors.left: parent.left
anchors.right: parent.right
wrapMode: Text.WordWrap
}
StatusCheckBox {
id: checkbox
visible: false
anchors.top: innerText.bottom
anchors.topMargin: Style.current.halfPadding
Layout.preferredWidth: parent.width
//% "Do not show this again"
text: qsTrId("do-not-show-this-again")
}
}
footer: Item {
id: footerContainer
width: parent.width
height: confirmButton.height//children[0].height
StatusButton {
id: confirmButton
type: confirmationDialog.btnType
anchors.right: cancelButton.visible ? cancelButton.left : parent.right
anchors.rightMargin: cancelButton.visible ? Style.current.smallPadding : 0
text: confirmationDialog.confirmButtonLabel
anchors.bottom: parent.bottom
onClicked: {
if (executeConfirm && typeof executeConfirm === "function") {
executeConfirm()
}
confirmationDialog.confirmButtonClicked()
}
}
StatusButton {
id: cancelButton
anchors.right: parent.right
visible: showCancelButton
anchors.rightMargin: Style.current.smallPadding
text: confirmationDialog.cancelButtonLabel
anchors.bottom: parent.bottom
onClicked: {
if (executeCancel && typeof executeCancel === "function") {
executeCancel()
}
confirmationDialog.cancelButtonClicked()
}
}
}
}

View File

@ -0,0 +1,78 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import "../imports"
import "../shared/status"
Rectangle {
id: copyToClipboardButton
height: 32
width: 32
radius: 8
color: Style.current.transparent
property var onClick: function() {}
property string textToCopy: ""
property bool tooltipUnder: false
Image {
width: 20
height: 20
sourceSize.width: width
sourceSize.height: height
source: "./img/copy-to-clipboard-icon.svg"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
ColorOverlay {
anchors.fill: parent
antialiasing: true
source: parent
color: Style.current.primary
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
hoverEnabled: true
onExited: {
parent.color = Style.current.transparent
}
onEntered:{
parent.color = Style.current.backgroundHover
}
onPressed: {
parent.color = Style.current.backgroundHover
if (!toolTip.visible) {
toolTip.visible = true
}
}
onReleased: {
parent.color = Style.current.backgroundHover
}
onClicked: {
if (textToCopy) {
chatsModel.copyToClipboard(textToCopy)
}
onClick()
}
}
StatusToolTip {
id: toolTip
//% "Copied!"
text: qsTrId("copied-")
orientation: tooltipUnder ? "bottom" : "top"
}
Timer {
id:hideTimer
interval: 2000
running: toolTip.visible
onTriggered: {
toolTip.visible = false;
}
}
}

195
ui/shared/Input.qml Normal file
View File

@ -0,0 +1,195 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../imports"
import "../shared/status"
import "."
Item {
property alias textField: inputValue
property string placeholderText: "My placeholder"
property string placeholderTextColor: Style.current.secondaryText
property alias text: inputValue.text
property alias maxLength: inputValue.maximumLength
property string validationError: ""
property alias validationErrorAlignment: validationErrorText.horizontalAlignment
property int validationErrorTopMargin: 1
property color validationErrorColor: Style.current.danger
property string label: ""
readonly property bool hasLabel: label !== ""
property color bgColor: Style.current.inputBackground
property url icon: ""
property int iconHeight: 24
property int iconWidth: 24
property bool copyToClipboard: false
property string textToCopy
property bool pasteFromClipboard: false
property bool readOnly: false
readonly property bool hasIcon: icon.toString() !== ""
readonly property var forceActiveFocus: function () {
inputValue.forceActiveFocus(Qt.MouseFocusReason)
}
readonly property int labelMargin: 7
property int customHeight: 44
property int fontPixelSize: 15
property alias validator: inputValue.validator
signal editingFinished(string inputValue)
signal textEdited(string inputValue)
id: inputBox
implicitHeight: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? (validationErrorText.height + validationErrorTopMargin) : 0)
height: implicitHeight
anchors.right: parent.right
anchors.left: parent.left
function resetInternal() {
inputValue.text = ""
validationError = ""
}
StyledText {
id: inputLabel
text: inputBox.label
font.weight: Font.Medium
anchors.left: parent.left
anchors.leftMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
font.pixelSize: 13
color: Style.current.textColor
}
Item {
id: inputField
anchors.right: parent.right
anchors.left: parent.left
height: customHeight
anchors.top: inputBox.hasLabel ? inputLabel.bottom : parent.top
anchors.topMargin: inputBox.hasLabel ? inputBox.labelMargin : 0
StyledTextField {
id: inputValue
visible: !inputBox.isTextArea && !inputBox.isSelect
placeholderText: inputBox.placeholderText
placeholderTextColor: inputBox.placeholderTextColor
anchors.top: parent.top
anchors.topMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.right: clipboardButtonLoader.active ? clipboardButtonLoader.left : parent.right
anchors.rightMargin: parent.rightMargin
anchors.left: parent.left
anchors.leftMargin: 0
leftPadding: inputBox.hasIcon ? iconWidth + 20 : Style.current.padding
selectByMouse: true
font.pixelSize: fontPixelSize
readOnly: inputBox.readOnly
background: Rectangle {
id: inputRectangle
anchors.fill: parent
color: bgColor
radius: Style.current.radius
border.width: (!!validationError || inputValue.focus) ? 1 : 0
border.color: {
if (!!validationError) {
return validationErrorColor
}
if (!inputBox.readOnly && inputValue.focus) {
return Style.current.inputBorderFocus
}
return Style.current.transparent
}
}
onEditingFinished: inputBox.editingFinished(inputBox.text)
onTextEdited: inputBox.textEdited(inputBox.text)
}
SVGImage {
id: iconImg
sourceSize.height: iconHeight
sourceSize.width: iconWidth
anchors.left: parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: inputBox.icon
}
Loader {
id: clipboardButtonLoader
active: inputBox.copyToClipboard || inputBox.pasteFromClipboard
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 8
sourceComponent: Component {
Item {
width: copyBtn.width
height: copyBtn.height
Timer {
id: timer
}
StatusButton {
property bool copied: false
id: copyBtn
text: {
if (copied) {
return inputBox.copyToClipboard ?
//% "Copied"
qsTrId("sharing-copied-to-clipboard") :
//% "Pasted"
qsTrId("pasted")
}
return inputBox.copyToClipboard ?
//% "Copy"
qsTrId("copy-to-clipboard") :
//% "Paste"
qsTrId("paste")
}
height: 28
font.pixelSize: 12
borderColor: Style.current.blue
showBorder: true
onClicked: {
if (inputBox.copyToClipboard) {
chatsModel.copyToClipboard(inputBox.textToCopy ? inputBox.textToCopy : inputValue.text)
} else {
if (inputValue.canPaste) {
inputValue.paste()
}
}
copyBtn.copied = true
timer.setTimeout(function() {
copyBtn.copied = false
}, 2000);
}
}
}
}
}
}
TextEdit {
visible: !!validationError
id: validationErrorText
text: validationError
anchors.top: inputField.bottom
anchors.topMargin: validationErrorTopMargin
anchors.right: inputField.right
selectByMouse: true
readOnly: true
font.pixelSize: 12
height: 16
color: validationErrorColor
wrapMode: TextEdit.Wrap
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#c0c0c0";formeditorZoom:1.25}
}
##^##*/

156
ui/shared/ModalPopup.qml Normal file
View File

@ -0,0 +1,156 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
Popup {
property string title
property bool noTopMargin: false
property bool displayCloseButton: true
default property alias content: popupContent.children
property alias contentWrapper: popupContent
property alias header: headerContent.children
id: popup
modal: true
property alias footer: footerContent.children
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.4)
}
closePolicy: displayCloseButton? Popup.CloseOnEscape | Popup.CloseOnPressOutside
: Popup.NoAutoClose
parent: Overlay.overlay
x: Math.round(((parent ? parent.width : 0) - width) / 2)
y: Math.round(((parent ? parent.height : 0) - height) / 2)
width: 480
height: 510 // TODO find a way to make this dynamic
background: Rectangle {
color: Style.current.background
radius: 8
}
onOpened: {
popupOpened = true
}
onClosed: {
popupOpened = false
}
padding: 0
contentItem: Item {
Item {
id: headerContent
height: {
const count = children.length
let h = 0
for (let i = 0; i < count; i++) {
h += children[i] ? children[i].height : 0
}
return h
}
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: popup.noTopMargin ? 0 : Style.current.padding
anchors.bottomMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
anchors.leftMargin: Style.current.padding
StyledText {
text: title
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
font.bold: true
font.pixelSize: 17
height: visible ? 24 : 0
visible: !!title
verticalAlignment: Text.AlignVCenter
}
}
Rectangle {
id: closeButton
property bool hovered: false
visible: displayCloseButton
height: 32
width: 32
anchors.top: parent.top
anchors.topMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
radius: 8
color: hovered ? Style.current.backgroundHover : Style.current.transparent
SVGImage {
id: closeModalImg
source: "./img/close.svg"
width: 11
height: 11
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
ColorOverlay {
anchors.fill: closeModalImg
source: closeModalImg
color: Style.current.textColor
}
MouseArea {
id: closeModalMouseArea
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
hoverEnabled: true
onExited: {
closeButton.hovered = false
}
onEntered: {
closeButton.hovered = true
}
onClicked: {
popup.close()
}
}
}
Separator {
id: separator
anchors.top: headerContent.bottom
anchors.topMargin: visible ? Style.current.padding : 0
visible: title.length > 0
}
Item {
id: popupContent
anchors.top: separator.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: separator2.top
anchors.bottomMargin: Style.current.padding
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
}
Separator {
id: separator2
visible: footerContent.visible && footerContent.height > 0
anchors.bottom: footerContent.top
anchors.bottomMargin: visible ? Style.current.padding : 0
}
Item {
id: footerContent
visible: children.length > 0
height: visible ? children[0] && children[0].height : 0
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: visible ? Style.current.padding : 0
anchors.rightMargin: visible ? Style.current.padding : 0
anchors.leftMargin: visible ? Style.current.padding : 0
}
}
}

161
ui/shared/PopupMenu.qml Normal file
View File

@ -0,0 +1,161 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import "../imports"
import "../shared"
Menu {
// This is to add icons to submenu items. QML doesn't have a way to add icons to those sadly so this is a workaround
property var subMenuIcons: []
property int paddingSize: 8
property bool hasArrow: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnReleaseOutside | Popup.CloseOnEscape
id: popupMenu
topPadding: paddingSize
bottomPadding: paddingSize
property string overrideTextColor: ""
delegate: MenuItem {
property color textColor: popupMenu.overrideTextColor !== "" ? popupMenu.overrideTextColor : (this.action.icon.color.toString() !== "#00000000" ? this.action.icon.color : Style.current.textColor)
property color hoverColor: popupMenuItem.action.icon.color === Style.current.danger ? Style.current.buttonWarnBackgroundColor : Style.current.backgroundHover
property int subMenuIndex: {
if (!this.subMenu) {
return -1
}
let child;
let index = 0;
for (let i = 0; i < popupMenu.count; i++) {
child = popupMenu.itemAt(i)
if (child.subMenu) {
if (child === this) {
return index
} else {
index++;
}
}
}
return index
}
enabled: {
if (this.subMenu) {
return this.subMenu.enabled
}
return this.action.enabled
}
action: Action{} // Meant to be overwritten
id: popupMenuItem
implicitWidth: 200
implicitHeight: 34
font.pixelSize: 13
font.weight: checked ? Font.Medium : Font.Normal
icon.color: popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.blue
icon.source: this.subMenu ? subMenuIcons[subMenuIndex].source : popupMenuItem.action.icon.source
icon.width: this.subMenu ? subMenuIcons[subMenuIndex].width : popupMenuItem.action.icon.width
icon.height: this.subMenu ? subMenuIcons[subMenuIndex].height : popupMenuItem.action.icon.height
visible: enabled
height: visible ? popupMenuItem.implicitHeight : 0
arrow: SVGImage {
source: "../app/img/caret.svg"
rotation: -90
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
width: 9
fillMode: Image.PreserveAspectFit
visible: popupMenuItem.subMenu && popupMenuItem.subMenu.enabled
ColorOverlay {
anchors.fill: parent
source: parent
color: popupMenuItem.textColor
}
}
// FIXME the icons looks very pixelated on Linux for some reason. Using smooth, mipmap, etc doesn't fix it
indicator: Item {
visible: !!popupMenuItem.icon.source.toString()
width: !isNaN(popupMenuItem.icon.width) ? popupMenuItem.icon.width : 25
height: !isNaN(popupMenuItem.icon.height) ? popupMenuItem.icon.height : 25
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
Image {
id: menuIcon
source: popupMenuItem.icon.source
visible: false
width: parent.width
height: parent.width
sourceSize.width: width
sourceSize.height: height
}
ColorOverlay {
anchors.fill: menuIcon
source: menuIcon
smooth: true
color: (popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.primaryMenuItemHover)
}
}
contentItem: StyledText {
anchors.left: popupMenuItem.indicator.right
anchors.leftMargin: popupMenu.paddingSize
text: popupMenuItem.text
font: popupMenuItem.font
color: popupMenuItem.textColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
opacity: enabled ? 1.0 : 0.3
elide: Text.ElideRight
}
background: Rectangle {
implicitWidth: 220
implicitHeight: enabled ? 24 : 0
color: popupMenuItem.hovered ? popupMenuItem.hoverColor : "transparent"
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}
background: Item {
id: bgPopupMenu
implicitWidth: 220
Rectangle {
id: bgPopupMenuContent
implicitWidth: bgPopupMenu.width
implicitHeight: bgPopupMenu.height
color: Style.current.modalBackground
radius: 8
layer.enabled: true
layer.effect: DropShadow{
width: bgPopupMenuContent.width
height: bgPopupMenuContent.height
x: bgPopupMenuContent.x
visible: bgPopupMenuContent.visible
source: bgPopupMenuContent
horizontalOffset: 0
verticalOffset: 4
radius: 12
samples: 25
spread: 0.2
color: "#22000000"
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

83
ui/shared/RoundedIcon.qml Normal file
View File

@ -0,0 +1,83 @@
import QtQuick 2.13
import QtGraphicalEffects 1.0
import "../imports"
Rectangle {
id: root
property alias source: roundedIconImage.source
default property alias content: content.children
property alias icon: roundedIconImage
property bool rotates: false
signal clicked
width: 36
height: 36
property alias iconWidth: roundedIconImage.width
property alias iconHeight: roundedIconImage.height
property alias rotation: roundedIconImage.rotation
property color iconColor: Style.current.transparent
color: Style.current.blue
radius: width / 2
Item {
id: iconContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: roundedIconImage.width
height: roundedIconImage.height
SVGImage {
id: roundedIconImage
width: 12
height: 12
fillMode: Image.PreserveAspectFit
source: "../img/new_chat.svg"
}
ColorOverlay {
anchors.fill: roundedIconImage
source: roundedIconImage
color: root.iconColor
rotation: roundedIconImage.rotation
}
}
Loader {
active: rotates
sourceComponent: rotatorComponent
}
Component {
id: rotatorComponent
RotationAnimator {
target: iconContainer
from: 0;
to: 360;
duration: 1200
running: true
loops: Animation.Infinite
}
}
Item {
id: content
anchors.left: iconContainer.right
anchors.leftMargin: 6 + (root.width - iconContainer.width)
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
}
}
}
/*##^##
Designer {
D{i:0;formeditorZoom:1.75}
}
##^##*/

View File

@ -0,0 +1,42 @@
import QtQuick 2.12
import QtGraphicalEffects 1.0
import "../imports"
Rectangle {
id: root
signal clicked
property bool noMouseArea: false
property bool noHover: false
property alias showLoadingIndicator: imgStickerPackThumb.showLoadingIndicator
property alias source: imgStickerPackThumb.source
property alias fillMode: imgStickerPackThumb.fillMode
radius: width / 2
width: 24
height: 24
color: Style.current.background
// apply rounded corners mask
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
x: root.x; y: root.y
width: root.width
height: root.height
radius: root.radius
}
}
ImageLoader {
id: imgStickerPackThumb
noMouseArea: root.noMouseArea
noHover: root.noHover
opacity: 1
smooth: false
radius: root.radius
anchors.fill: parent
source: "https://ipfs.infura.io/ipfs/" + thumbnail
onClicked: root.clicked()
}
}

9
ui/shared/SVGImage.qml Normal file
View File

@ -0,0 +1,9 @@
import QtQuick 2.13
Image {
sourceSize.width: width || undefined
sourceSize.height: height || undefined
fillMode: Image.PreserveAspectFit
mipmap: true
antialiasing: true
}

17
ui/shared/Separator.qml Normal file
View File

@ -0,0 +1,17 @@
import QtQuick 2.13
import "../imports"
Item {
id: root
property color color: Style.current.separator
width: parent.width
height: root.visible ? 1 : 0
anchors.topMargin: Style.current.padding
Rectangle {
id: separator
width: parent.width
height: 1
color: root.color
anchors.verticalCenter: parent.verticalCenter
}
}

View File

@ -0,0 +1,46 @@
import QtQuick 2.13
import QtGraphicalEffects 1.13
import "../imports"
Item {
property int iconMargin: Style.current.padding
property alias icon: icon
readonly property int separatorWidth: (parent.width / 2) - (icon.height / 2) - iconMargin
width: parent.width
height: icon.height
Separator {
id: separatorLeft
width: separatorWidth
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.topMargin: undefined
}
SVGImage {
id: icon
height: 14
width: 18
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/arrow-right.svg"
rotation: 90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.textColor
antialiasing: true
}
}
Separator {
id: separatorRight
width: separatorWidth
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.topMargin: undefined
}
}

View File

@ -0,0 +1,46 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQml 2.14
import "../imports"
Button {
property string label: "My button"
property color btnColor: Style.current.secondaryBackground
property color btnBorderColor: "transparent"
property int btnBorderWidth: 0
property color textColor: Style.current.blue
property int textSize: 15
property bool disabled: false
id: btnStyled
width: txtBtnLabel.width + 2 * Style.current.padding
height: 44
enabled: !disabled
background: Rectangle {
color: disabled ? Style.current.grey :
hovered ? Qt.darker(btnStyled.btnColor, 1.1) : btnStyled.btnColor
radius: Style.current.radius
anchors.fill: parent
border.color: btnBorderColor
border.width: btnBorderWidth
}
StyledText {
id: txtBtnLabel
color: btnStyled.disabled ? Style.current.darkGrey : btnStyled.textColor
font.pixelSize: btnStyled.textSize
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: btnStyled.label
font.weight: Font.Medium
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
parent.onClicked()
}
}
}

7
ui/shared/StyledText.qml Normal file
View File

@ -0,0 +1,7 @@
import QtQuick 2.13
import "../imports"
Text {
font.family: Style.current.fontRegular.name
color: Style.current.textColor
}

View File

@ -0,0 +1,9 @@
import QtQuick 2.13
import "../imports"
TextEdit {
font.family: Style.current.fontRegular.name
color: Style.current.textColor
selectedTextColor: Style.current.textColor
selectionColor: Style.current.primarySelectionColor
}

View File

@ -0,0 +1,11 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../imports"
TextField {
font.family: Style.current.fontRegular.name
color: readOnly ? Style.current.secondaryText : Style.current.textColor
selectByMouse: !readOnly
selectedTextColor: Style.current.textColor
selectionColor: Style.current.primarySelectionColor
}

View File

@ -0,0 +1,39 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../imports"
Button {
id: root
property alias label: txtBtnLabel.text
width: txtBtnLabel.width + 2 * 12
height: txtBtnLabel.height + 2 * 6
background: Rectangle {
color: Style.current.backgroundTertiary
radius: 6
anchors.fill: parent
border.color: Style.current.borderTertiary
border.width: 1
}
StyledText {
id: txtBtnLabel
color: Style.current.textColorTertiary
font.pixelSize: 12
height: 16
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
//% "Paste"
text: qsTrId("paste")
}
MouseArea {
id: mouse
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
parent.clicked()
}
}
}

View File

@ -0,0 +1,61 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../imports"
Item {
property string text: "My Text"
property string label: "My Label"
property string fontFamily: Style.current.fontRegular.name
property string textToCopy: ""
property alias value: textItem
property bool wrap: false
id: infoText
implicitHeight: this.childrenRect.height
width: parent.width
StyledText {
id: inputLabel
text: infoText.label
font.weight: Font.Medium
font.pixelSize: 13
color: Style.current.secondaryText
}
StyledTextEdit {
id: textItem
text: infoText.text
selectByMouse: true
font.family: fontFamily
readOnly: true
anchors.top: inputLabel.bottom
anchors.topMargin: 4
font.pixelSize: 15
wrapMode: infoText.wrap ? Text.WordWrap : Text.NoWrap
anchors.left: parent.left
anchors.right: infoText.wrap ? parent.right : undefined
}
Loader {
active: !!infoText.textToCopy
sourceComponent: copyComponent
anchors.verticalCenter: textItem.verticalCenter
anchors.left: textItem.right
anchors.leftMargin: Style.current.smallPadding
}
Component {
id: copyComponent
CopyToClipBoardButton {
textToCopy: infoText.textToCopy
}
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#ffffff";formeditorZoom:1.25}
}
##^##*/

15
ui/shared/Timer.qml Normal file
View File

@ -0,0 +1,15 @@
import QtQuick 2.13
Timer {
id: timer
function setTimeout(cb, delayTime) {
timer.interval = delayTime;
timer.repeat = false;
timer.triggered.connect(cb);
timer.triggered.connect(function release () {
timer.triggered.disconnect(cb); // This is important
timer.triggered.disconnect(release); // This is important as well
});
timer.start();
}
}

3
ui/shared/img/close.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4697 11.5303C10.7626 11.8232 11.2374 11.8232 11.5303 11.5303C11.8232 11.2374 11.8232 10.7626 11.5303 10.4697L7.41421 6.35355C7.21895 6.15829 7.21895 5.84171 7.41421 5.64645L11.5303 1.53033C11.8232 1.23744 11.8232 0.762564 11.5303 0.46967C11.2374 0.176777 10.7626 0.176777 10.4697 0.46967L6.35355 4.58579C6.15829 4.78105 5.84171 4.78105 5.64645 4.58579L1.53033 0.46967C1.23744 0.176777 0.762563 0.176777 0.46967 0.46967C0.176777 0.762563 0.176777 1.23744 0.46967 1.53033L4.58579 5.64645C4.78105 5.84171 4.78105 6.15829 4.58579 6.35355L0.46967 10.4697C0.176777 10.7626 0.176777 11.2374 0.46967 11.5303C0.762563 11.8232 1.23744 11.8232 1.53033 11.5303L5.64645 7.41421C5.84171 7.21895 6.15829 7.21895 6.35355 7.41421L10.4697 11.5303Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 862 B

25
ui/shared/qmldir Normal file
View File

@ -0,0 +1,25 @@
StyledButton 1.0 StyledButton.qml
RoundedIcon 1.0 RoundedIcon.qml
ModalPopup 1.0 ModalPopup.qml
PopupMenu 1.0 PopupMenu.qml
Separator 1.0 Separator.qml
StatusTabButton 1.0 StatusTabButton.qml
TextWithLabel 1.0 TextWithLabel.qml
Input 1.0 Input.qml
SearchBox 1.0 SearchBox.qml
Select 1.0 Select.qml
StyledTextArea 1.0 StyledTextArea.qml
StyledText 1.0 StyledText.qml
StyledTextField 1.0 StyledTextField.qml
StyledTextEdit 1.0 StyledTextEdit.qml
Identicon 1.0 Identicon.qml
RoundedImage 1.0 RoundedImage.qml
SplitViewHandle 1.0 SplitViewHandle.qml
CopyToClipBoardButton 1.0 CopyToClipBoardButton.qml
NotificationWindow 1.0 NotificationWindow.qml
BlockContactConfirmationDialog 1.0 BlockContactConfirmationDialog.qml
ConfirmationDialog 1.0 ConfirmationDialog.qml
Timer 1.0 Timer.qml
TransactionSigner 1.0 TransactionSigner.qml
GlossaryEntry 1.0 GlossaryEntry.qml
GlossaryLetter 1.0 GlossaryLetter.qml

View File

@ -0,0 +1,141 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQml 2.14
import QtGraphicalEffects 1.13
import "../../imports"
import "../../shared"
import "./core"
Button {
property string type: "primary"
property string size: "large"
property string state: "default"
property color color: type === "warn" ? Style.current.danger : Style.current.buttonForegroundColor
property color bgColor: type === "warn" ? Style.current.buttonWarnBackgroundColor : Style.current.buttonBackgroundColor
property color borderColor: color
property color hoveredBorderColor: color
property bool forceBgColorOnHover: false
property int borderRadius: Style.current.radius
property color bgHoverColor: {
if (type === "warn") {
if (showBorder) {
return Style.current.buttonOutlineHoveredWarnBackgroundColor
}
return Style.current.buttonHoveredWarnBackgroundColor
}
return Style.current.buttonBackgroundColorHover
}
property bool disableColorOverlay: false
property bool showBorder: false
property int iconRotation: 0
id: control
font.pixelSize: size === "small" ? 13 : 15
font.family: Style.current.fontRegular.name
font.weight: Font.Medium
implicitHeight: flat ? 32 : (size === "small" ? 38 : 44)
implicitWidth: buttonLabel.implicitWidth + (flat ? 3* Style.current.halfPadding : 2 * Style.current.padding) +
(iconLoader.active ? iconLoader.width : 0)
enabled: state === "default"
contentItem: Item {
id: content
anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter
Loader {
id: iconLoader
active: !!control.icon && !!control.icon.source.toString()
anchors.left: parent.left
anchors.leftMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
sourceComponent: SVGImage {
id: iconImg
source: control.icon.source
height: control.icon.height
width: control.icon.width
fillMode: Image.PreserveAspectFit
rotation: control.iconRotation
ColorOverlay {
enabled: !control.disableColorOverlay
anchors.fill: iconImg
source: iconImg
color: control.disableColorOverlay ? "transparent" : buttonLabel.color
antialiasing: true
smooth: true
rotation: control.iconRotation
}
}
}
Text {
id: buttonLabel
text: control.text
font: control.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: iconLoader.active ? undefined : parent.right
anchors.left: iconLoader.active ? iconLoader.right : parent.left
anchors.leftMargin: iconLoader.active ? Style.current.smallPadding : 0
color: {
if (!enabled) {
return Style.current.buttonDisabledForegroundColor
} else if (type !== "warn" && (hovered || highlighted)) {
return control.color !== Style.current.buttonForegroundColor ?
control.color : Style.current.blue
}
return control.color
}
visible: !loadingIndicator.active
}
Loader {
id: loadingIndicator
active: control.state === "pending"
sourceComponent: StatusLoadingIndicator {}
height: loadingIndicator.visible ?
control.size === "large" ?
23 : 17
: 0
width: loadingIndicator.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
background: Rectangle {
radius: borderRadius
anchors.fill: parent
border.width: flat || showBorder ? 1 : 0
border.color: {
if (hovered) {
return control.hoveredBorderColor !== control.borderColor ? control.hoveredBorderColor : control.borderColor
}
if (showBorder && enabled) {
return control.borderColor
}
return Style.current.transparent
}
color: {
if (flat) {
return hovered && forceBgColorOnHover ? control.bgHoverColor : "transparent"
}
if (type === "secondary") {
return hovered || control.highlighted ? control.bgColor : "transparent"
}
return !enabled ? (control.bgColor === Style.current.transparent ? control.bgColor : Style.current.buttonDisabledBackgroundColor) :
(hovered ? control.bgHoverColor : control.bgColor)
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}

View File

@ -0,0 +1,36 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.13
import "../../imports"
import "../../shared"
CheckBox {
id: control
indicator: Rectangle {
implicitWidth: 18
implicitHeight: 18
x: control.leftPadding
y: parent.height / 2 - height / 2
radius: 3
color: (control.down || control.checked) ? Style.current.primary : Style.current.inputBackground
SVGImage {
source: "../img/checkmark.svg"
width: 16
height: 16
anchors.centerIn: parent
visible: control.down || control.checked
}
}
contentItem: StyledText {
text: control.text
opacity: enabled ? 1.0 : 0.3
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
width: parent.width
leftPadding: !!control.text ? control.indicator.width + control.spacing : control.indicator.width
}
}

View File

@ -0,0 +1,12 @@
import QtQuick 2.13
import "../../imports"
import "../../shared"
StatusIconButton {
id: moreActionsBtn
anchors.verticalCenter: parent.verticalCenter
icon.name: "dots-icon"
iconColor: Style.current.contextMenuButtonForegroundColor
hoveredIconColor: Style.current.contextMenuButtonForegroundColor
highlightedBackgroundColor: Style.current.contextMenuButtonBackgroundHoverColor
}

View File

@ -0,0 +1,54 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQml 2.14
import "../../imports"
import "../../shared"
RadioButton {
id: control
property bool isHovered: false
property bool enabled: true
width: indicator.implicitWidth
function getColor() {
if (!enabled) {
return checked ? Style.current.darkGrey : Style.current.grey
}
if (checked) {
return Style.current.blue
}
if (hovered || isHovered) {
return Style.current.secondaryHover
}
return Style.current.grey
}
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
x: 0
y: 6
radius: 10
color: control.getColor()
Rectangle {
width: 12
height: 12
radius: 6
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: control.checked ? Style.current.white : Style.current.grey
visible: control.checked
}
}
contentItem: StyledText {
text: control.text
color: Style.current.textColor
verticalAlignment: Text.AlignVCenter
leftPadding: !!control.text ? control.indicator.width + control.spacing : control.indicator.width
font.pixelSize: 15
font.family: Style.current.fontRegular.name
}
}

View File

@ -0,0 +1,63 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../imports"
import ".."
import "."
Rectangle {
property alias text: textElement.text
property var buttonGroup
property bool checked: false
property bool isHovered: false
signal radioCheckedChanged(checked: bool)
id: root
height: 52
color: isHovered ? Style.current.backgroundHover : Style.current.transparent
radius: Style.current.radius
border.width: 0
anchors.left: parent.left
anchors.leftMargin: -Style.current.padding
anchors.right: parent.right
anchors.rightMargin: -Style.current.padding
StyledText {
id: textElement
text: ""
font.pixelSize: 15
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
hoverEnabled: true
onEntered: root.isHovered = true
onExited: root.isHovered = false
onClicked: {
radioButton.checked = true
}
}
StatusRadioButton {
id: radioButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
ButtonGroup.group: root.buttonGroup
rightPadding: 0
checked: root.checked
onCheckedChanged: root.radioCheckedChanged(checked)
MouseArea {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
onEntered: root.isHovered = true
}
}
}

View File

@ -0,0 +1,207 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import QtQml 2.14
import "../../imports"
import "../../shared"
import "./core"
RoundButton {
property string type: "primary"
property string size: "large"
property int pressedIconRotation: 0
property alias iconX: iconImg.x
id: control
font.pixelSize: 15
font.weight: Font.Medium
implicitWidth: {
switch(size) {
case "large":
return 44
case "medium":
return 40
case "small":
return 32
default:
return 44
}
}
implicitHeight: implicitWidth
enabled: state === "default" || state === "pressed"
rotation: 0
state: "default"
states: [
State {
name: "default"
PropertyChanges {
target: iconColorOverlay
visible: true
rotation: 0
}
PropertyChanges {
target: loadingIndicator
active: false
}
},
State {
name: "pressed"
PropertyChanges {
target: iconColorOverlay
rotation: control.pressedIconRotation
visible: true
}
PropertyChanges {
target: loadingIndicator
active: false
}
},
State {
name: "pending"
PropertyChanges {
target: loadingIndicator
active: true
}
PropertyChanges {
target: iconColorOverlay
visible: false
}
}
]
transitions: [
Transition {
from: "default"
to: "pressed"
RotationAnimation {
duration: 150
direction: RotationAnimation.Clockwise
easing.type: Easing.InCubic
}
},
Transition {
from: "pressed"
to: "default"
RotationAnimation {
duration: 150
direction: RotationAnimation.Counterclockwise
easing.type: Easing.OutCubic
}
}
]
icon.height: {
switch(size) {
case "large":
return 20
case "medium":
return 14
case "small":
return 12
default:
return 20
}
}
icon.width: {
switch(size) {
case "large":
return 20
case "medium":
return 14
case "small":
return 12
default:
return 20
}
}
icon.color: type === "secondary" ?
!enabled ?
Style.current.roundedButtonSecondaryDisabledForegroundColor :
Style.current.roundedButtonSecondaryForegroundColor
:
!enabled ?
Style.current.roundedButtonDisabledForegroundColor :
Style.current.roundedButtonForegroundColor
onIconChanged: {
icon.source = icon.name ? "../../app/img/" + icon.name + ".svg" : ""
}
background: Rectangle {
anchors.fill: parent
opacity: hovered && size === "large" && type !== "secondary" ? 0.2 : 1
color: {
if (size === "medium" || size === "small" || type === "secondary") {
return !enabled ? Style.current.roundedButtonSecondaryDisabledBackgroundColor :
hovered ? (control.type === "warn" ? Style.current.red : Style.current.roundedButtonSecondaryHoveredBackgroundColor) :
(control.type === "warn" ? Style.current.lightRed : Style.current.roundedButtonSecondaryBackgroundColor)
}
return !enabled ?
Style.current.roundedButtonDisabledBackgroundColor :
hovered ? (control.type === "warn" ? Style.current.red : Style.current.buttonHoveredBackgroundColor) :
(control.type === "warn" ? Style.current.lightRed : Style.current.roundedButtonBackgroundColor)
}
radius: parent.width / 2
}
contentItem: Item {
anchors.fill: parent
SVGImage {
id: iconImg
visible: false
source: control.icon.source
height: control.icon.height
width: control.icon.width
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
}
Component {
id: loadingComponent
StatusLoadingIndicator {
color: control.size === "medium" || control.size === "small" ?
Style.current.roundedButtonSecondaryDisabledForegroundColor :
Style.current.roundedButtonDisabledForegroundColor
}
}
Loader {
id: loadingIndicator
sourceComponent: loadingComponent
height: size === "small" ? 14 : 18
width: loadingIndicator.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
ColorOverlay {
id: iconColorOverlay
anchors.fill: iconImg
source: iconImg
color: {
if (type === "secondary") {
return !control.enabled ?
Style.current.roundedButtonSecondaryDisabledForegroundColor :
(control.type === "warn" ? Style.current.danger : Style.current.roundedButtonSecondaryForegroundColor)
}
return !control.enabled ?
Style.current.roundedButtonDisabledForegroundColor :
(control.type === "warn" ? Style.current.danger : Style.current.roundedButtonForegroundColor)
}
antialiasing: true
}
}
MouseArea {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}

View File

@ -0,0 +1,11 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../imports"
import "../../shared"
StyledText {
font.pixelSize: 15
color: Style.current.secondaryText
anchors.topMargin: 38
}

View File

@ -0,0 +1,127 @@
import QtQuick 2.13
import QtGraphicalEffects 1.12
import "../../imports"
import ".."
Rectangle {
property string text
property bool isSwitch: false
property bool switchChecked: false
property string currentValue
property bool isBadge: false
property string badgeText: "1"
property int badgeRadius: 9
property bool isEnabled: true
signal clicked(bool checked)
property bool isHovered: false
property int badgeSize: 18
property url iconSource
id: root
implicitHeight: 52
color: isHovered ? Style.current.backgroundHover : Style.current.transparent
radius: Style.current.radius
border.width: 0
anchors.left: parent.left
anchors.leftMargin: -Style.current.padding
anchors.right: parent.right
anchors.rightMargin: -Style.current.padding
RoundedIcon {
id: pinImage
visible: !!root.iconSource.toString()
source: root.iconSource
iconColor: Style.current.primary
color: Style.current.secondaryBackground
width: 40
height: 40
iconWidth: 24
iconHeight: 24
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: textItem
anchors.left: pinImage.visible ? pinImage.right : parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
text: root.text
font.pixelSize: 15
color: !root.isEnabled ? Style.current.secondaryText : Style.current.textColor
}
StyledText {
id: valueText
visible: !!root.currentValue
text: root.currentValue
elide: Text.ElideRight
font.pixelSize: 15
horizontalAlignment: Text.AlignRight
color: Style.current.secondaryText
anchors.left: textItem.right
anchors.leftMargin: Style.current.padding
anchors.right: root.isSwitch ? switchItem.left : caret.left
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: textItem.verticalCenter
}
StatusSwitch {
id: switchItem
enabled: root.isEnabled
visible: root.isSwitch
checked: root.switchChecked
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: textItem.verticalCenter
}
Rectangle {
id: badge
visible: root.isBadge & !root.isSwitch
anchors.right: root.isSwitch ? switchItem.left : caret.left
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: textItem.verticalCenter
radius: root.badgeRadius
color: Style.current.blue
width: root.badgeSize
height: root.badgeSize
Text {
font.pixelSize: 12
color: Style.current.white
anchors.centerIn: parent
text: root.badgeText
}
}
SVGImage {
id: caret
visible: !root.isSwitch
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: textItem.verticalCenter
source: "../../app/img/caret.svg"
width: 13
height: 7
rotation: -90
ColorOverlay {
anchors.fill: caret
source: caret
color: Style.current.secondaryText
}
}
MouseArea {
anchors.fill: parent
enabled: root.isEnabled
hoverEnabled: true
onEntered: root.isHovered = true
onExited: root.isHovered = false
onClicked: {
root.clicked(!root.switchChecked)
}
cursorShape: isEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}

View File

@ -0,0 +1,64 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.13
import "../../imports"
import "../../shared"
Switch {
id: control
indicator: Rectangle {
id: oval
implicitWidth: 52
implicitHeight: 28
x: control.leftPadding
y: parent.height / 2 - height / 2
radius: 14
color: control.checked ? Style.current.primary : Style.current.inputBackground
Rectangle {
id: circle
y: 4
width: 20
height: 20
radius: 10
color: Style.current.white
layer.enabled: true
layer.effect: DropShadow {
width: parent.width
height: parent.height
visible: true
verticalOffset: 1
fast: true
cached: true
color: "#22000000"
}
states: [
State {
name: "on"
when: control.checked
PropertyChanges { target: circle; x: oval.width - circle.width - 4 }
},
State {
name: "off"
when: !control.checked
PropertyChanges { target: circle; x: 4 }
}
]
transitions: Transition {
reversible: true
NumberAnimation { properties: "x"; easing.type: Easing.Linear; duration: 120; }
}
}
}
contentItem: StyledText {
text: control.text
opacity: enabled ? 1.0 : 0.3
verticalAlignment: Text.AlignVCenter
leftPadding: !!control.text ? control.indicator.width + control.spacing : control.indicator.width
}
}

View File

@ -0,0 +1,31 @@
import QtQuick 2.13
import QtGraphicalEffects 1.13
Image {
property string icon: ""
property color color
id: root
width: 24
height: 24
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
onIconChanged: {
if (icon !== "") {
source = "../assets/img/icons/" + icon + ".svg";
}
}
ColorOverlay {
visible: root.color !== undefined
anchors.fill: root
source: root
color: root.color
antialiasing: true
smooth: true
rotation: root.rotation
}
}

View File

@ -0,0 +1,17 @@
import QtQuick 2.13
import "."
StatusIcon {
id: root
icon: "loading"
height: 17
width: 17
RotationAnimator {
target: root;
from: 0;
to: 360;
duration: 1200
running: true
loops: Animation.Infinite
}
}

24
ui/shared/status/qmldir Normal file
View File

@ -0,0 +1,24 @@
StatusButton 1.0 StatusButton.qml
StatusChatCommandButton 1.0 StatusChatCommandButton.qml
StatusChatCommandPopup 1.0 StatusChatCommandPopup.qml
StatusChatInput 1.0 StatusChatInput.qml
StatusCategoryButton 1.0 StatusCategoryButton.qml
StatusEmojiPopup 1.0 StatusEmojiPopup.qml
StatusEmojiSection 1.0 StatusEmojiSection.qml
StatusGifPopup 1.0 StatusGifPopup.qml
StatusGifColumn 1.0 StatusGifColumn.qml
StatusIconButton 1.0 StatusIconButton.qml
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
StatusRadioButton 1.0 StatusRadioButton.qml
StatusRoundButton 1.0 StatusRoundButton.qml
StatusSectionHeadline 1.0 StatusSectionHeadline.qml
StatusSectionMenuItem 1.0 StatusSectionMenuItem.qml
StatusSlider 1.0 StatusSlider.qml
StatusStickerButton 1.0 StatusStickerButton.qml
StatusStickerList 1.0 StatusStickerList.qml
StatusStickerMarket 1.0 StatusStickerMarket.qml
StatusStickerPackDetails 1.0 StatusStickerPackDetails.qml
StatusStickerPackPurchaseModal 1.0 StatusStickerPackPurchaseModal.qml
StatusStickersPopup 1.0 StatusStickersPopup.qml
StatusToolTip 1.0 StatusToolTip.qml

1
vendor/nim-confutils vendored Submodule

@ -0,0 +1 @@
Subproject commit ab4ba1cbfdccdb8c0398894ffc25169bc61faeed

@ -1 +1 @@
Subproject commit 2b6e50491786ae0d61a97f99edda27b70364838a
Subproject commit 5d20a34714d1e4df286eb423e5447adc955bcffa

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 0e8c7eef73427c78c413637c74ba8c1031fdb20c
Subproject commit 83c1e3c84b02665838fd4b3aea91f4e80fc887fb