From ed04a04db7f65d46bb0bbb0cd94cd9e7933c1ea1 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 18 Aug 2021 15:59:52 -0400 Subject: [PATCH] start/stop node --- .gitignore | 3 +- Makefile | 9 +- src/app/node/core.nim | 37 +++ src/app/node/view.nim | 78 ++++++ src/status/constants.nim | 5 + src/status/libstatus/accounts.nim | 13 + src/status/libstatus/accounts/constants.nim | 57 ++++ src/status/libstatus/core.nim | 28 ++ src/status/libstatus/settings.nim | 22 ++ src/status/node.nim | 16 ++ src/status/settings.nim | 25 ++ src/status/signals/core.nim | 58 ++++ src/status/signals/stats.nim | 13 + src/status/signals/types.nim | 19 ++ src/status/status.nim | 30 ++ src/status/tasks/common.nim | 17 ++ src/status/tasks/marathon.nim | 43 +++ src/status/tasks/marathon/common.nim | 6 + src/status/tasks/marathon/worker.nim | 49 ++++ src/status/tasks/qt.nim | 16 ++ src/status/tasks/task_runner_impl.nim | 28 ++ src/status/tasks/threadpool.nim | 257 ++++++++++++++++++ src/status/types.nim | 70 +++++ src/status/utils.nim | 112 ++++++++ src/status_node.nim | 61 ++--- ui/app/AppLayout.qml | 215 +++++++++++++++ ui/app/BloomSelectorButton.qml | 70 +++++ ui/app/FleetRadioSelector.qml | 38 +++ ui/app/FleetsModal.qml | 49 ++++ ui/app/Rate.qml | 78 ++++++ ui/app/img/caret.svg | 3 + ui/app/img/traffic_lights/close.png | Bin 0 -> 493 bytes ui/app/img/traffic_lights/close_pressed.png | Bin 0 -> 481 bytes ui/app/img/traffic_lights/maximize.png | Bin 0 -> 340 bytes .../img/traffic_lights/maximize_pressed.png | Bin 0 -> 299 bytes ui/app/img/traffic_lights/minimise.png | Bin 0 -> 222 bytes .../img/traffic_lights/minimise_pressed.png | Bin 0 -> 214 bytes ui/imports/Config.qml | 7 - ui/imports/qmldir | 4 + ui/main.qml | 89 +++--- ui/shared/ConfirmationDialog.qml | 101 +++++++ ui/shared/CopyToClipBoardButton.qml | 78 ++++++ ui/shared/Input.qml | 195 +++++++++++++ ui/shared/ModalPopup.qml | 156 +++++++++++ ui/shared/PopupMenu.qml | 161 +++++++++++ ui/shared/RoundedIcon.qml | 83 ++++++ ui/shared/RoundedImage.qml | 42 +++ ui/shared/SVGImage.qml | 9 + ui/shared/Separator.qml | 17 ++ ui/shared/SeparatorWithIcon.qml | 46 ++++ ui/shared/StyledButton.qml | 46 ++++ ui/shared/StyledText.qml | 7 + ui/shared/StyledTextEdit.qml | 9 + ui/shared/StyledTextField.qml | 11 + ui/shared/TertiaryButton.qml | 39 +++ ui/shared/TextWithLabel.qml | 61 +++++ ui/shared/Timer.qml | 15 + ui/shared/img/close.svg | 3 + ui/shared/qmldir | 25 ++ ui/shared/status/StatusButton.qml | 141 ++++++++++ ui/shared/status/StatusCheckBox.qml | 36 +++ ui/shared/status/StatusContextMenuButton.qml | 12 + ui/shared/status/StatusRadioButton.qml | 54 ++++ ui/shared/status/StatusRadioButtonRow.qml | 63 +++++ ui/shared/status/StatusRoundButton.qml | 207 ++++++++++++++ ui/shared/status/StatusSectionHeadline.qml | 11 + ui/shared/status/StatusSettingsLineButton.qml | 127 +++++++++ ui/shared/status/StatusSwitch.qml | 64 +++++ ui/shared/status/core/StatusIcon.qml | 31 +++ .../status/core/StatusLoadingIndicator.qml | 17 ++ ui/shared/status/qmldir | 24 ++ vendor/nim-confutils | 1 + vendor/nim-status-go | 2 +- vendor/status-go | 2 +- 74 files changed, 3423 insertions(+), 98 deletions(-) create mode 100644 src/app/node/core.nim create mode 100644 src/app/node/view.nim create mode 100644 src/status/constants.nim create mode 100644 src/status/libstatus/accounts.nim create mode 100644 src/status/libstatus/accounts/constants.nim create mode 100644 src/status/libstatus/core.nim create mode 100644 src/status/libstatus/settings.nim create mode 100644 src/status/node.nim create mode 100644 src/status/settings.nim create mode 100644 src/status/signals/core.nim create mode 100644 src/status/signals/stats.nim create mode 100644 src/status/signals/types.nim create mode 100644 src/status/status.nim create mode 100644 src/status/tasks/common.nim create mode 100644 src/status/tasks/marathon.nim create mode 100644 src/status/tasks/marathon/common.nim create mode 100644 src/status/tasks/marathon/worker.nim create mode 100644 src/status/tasks/qt.nim create mode 100644 src/status/tasks/task_runner_impl.nim create mode 100644 src/status/tasks/threadpool.nim create mode 100644 src/status/types.nim create mode 100644 src/status/utils.nim create mode 100644 ui/app/AppLayout.qml create mode 100644 ui/app/BloomSelectorButton.qml create mode 100644 ui/app/FleetRadioSelector.qml create mode 100644 ui/app/FleetsModal.qml create mode 100644 ui/app/Rate.qml create mode 100644 ui/app/img/caret.svg create mode 100644 ui/app/img/traffic_lights/close.png create mode 100644 ui/app/img/traffic_lights/close_pressed.png create mode 100644 ui/app/img/traffic_lights/maximize.png create mode 100644 ui/app/img/traffic_lights/maximize_pressed.png create mode 100644 ui/app/img/traffic_lights/minimise.png create mode 100644 ui/app/img/traffic_lights/minimise_pressed.png delete mode 100644 ui/imports/Config.qml create mode 100644 ui/imports/qmldir create mode 100644 ui/shared/ConfirmationDialog.qml create mode 100644 ui/shared/CopyToClipBoardButton.qml create mode 100644 ui/shared/Input.qml create mode 100644 ui/shared/ModalPopup.qml create mode 100644 ui/shared/PopupMenu.qml create mode 100644 ui/shared/RoundedIcon.qml create mode 100644 ui/shared/RoundedImage.qml create mode 100644 ui/shared/SVGImage.qml create mode 100644 ui/shared/Separator.qml create mode 100644 ui/shared/SeparatorWithIcon.qml create mode 100644 ui/shared/StyledButton.qml create mode 100644 ui/shared/StyledText.qml create mode 100644 ui/shared/StyledTextEdit.qml create mode 100644 ui/shared/StyledTextField.qml create mode 100644 ui/shared/TertiaryButton.qml create mode 100644 ui/shared/TextWithLabel.qml create mode 100644 ui/shared/Timer.qml create mode 100644 ui/shared/img/close.svg create mode 100644 ui/shared/qmldir create mode 100644 ui/shared/status/StatusButton.qml create mode 100644 ui/shared/status/StatusCheckBox.qml create mode 100644 ui/shared/status/StatusContextMenuButton.qml create mode 100644 ui/shared/status/StatusRadioButton.qml create mode 100644 ui/shared/status/StatusRadioButtonRow.qml create mode 100644 ui/shared/status/StatusRoundButton.qml create mode 100644 ui/shared/status/StatusSectionHeadline.qml create mode 100644 ui/shared/status/StatusSettingsLineButton.qml create mode 100644 ui/shared/status/StatusSwitch.qml create mode 100644 ui/shared/status/core/StatusIcon.qml create mode 100644 ui/shared/status/core/StatusLoadingIndicator.qml create mode 100644 ui/shared/status/qmldir create mode 160000 vendor/nim-confutils diff --git a/.gitignore b/.gitignore index 4ce6f44..a277e38 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ resources.qrc status-react-translations/ /.update.timestamp notarization.log -status-desktop.log \ No newline at end of file +DesktopNode +status-node.log diff --git a/Makefile b/Makefile index ec7f588..2e4ee1b 100644 --- a/Makefile +++ b/Makefile @@ -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)" diff --git a/src/app/node/core.nim b/src/app/node/core.nim new file mode 100644 index 0000000..841e7bd --- /dev/null +++ b/src/app/node/core.nim @@ -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) \ No newline at end of file diff --git a/src/app/node/view.nim b/src/app/node/view.nim new file mode 100644 index 0000000..3b2036b --- /dev/null +++ b/src/app/node/view.nim @@ -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() diff --git a/src/status/constants.nim b/src/status/constants.nim new file mode 100644 index 0000000..03efacb --- /dev/null +++ b/src/status/constants.nim @@ -0,0 +1,5 @@ +import libstatus/accounts/constants + +export DATADIR +export STATUSGODIR +export KEYSTOREDIR diff --git a/src/status/libstatus/accounts.nim b/src/status/libstatus/accounts.nim new file mode 100644 index 0000000..bb7e27b --- /dev/null +++ b/src/status/libstatus/accounts.nim @@ -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) diff --git a/src/status/libstatus/accounts/constants.nim b/src/status/libstatus/accounts/constants.nim new file mode 100644 index 0000000..715afe9 --- /dev/null +++ b/src/status/libstatus/accounts/constants.nim @@ -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) + diff --git a/src/status/libstatus/core.nim b/src/status/libstatus/core.nim new file mode 100644 index 0000000..d240724 --- /dev/null +++ b/src/status/libstatus/core.nim @@ -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 + diff --git a/src/status/libstatus/settings.nim b/src/status/libstatus/settings.nim new file mode 100644 index 0000000..fd47920 --- /dev/null +++ b/src/status/libstatus/settings.nim @@ -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 diff --git a/src/status/node.nim b/src/status/node.nim new file mode 100644 index 0000000..23c1b5c --- /dev/null +++ b/src/status/node.nim @@ -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) diff --git a/src/status/settings.nim b/src/status/settings.nim new file mode 100644 index 0000000..c0c6495 --- /dev/null +++ b/src/status/settings.nim @@ -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() \ No newline at end of file diff --git a/src/status/signals/core.nim b/src/status/signals/core.nim new file mode 100644 index 0000000..ed33dd3 --- /dev/null +++ b/src/status/signals/core.nim @@ -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) diff --git a/src/status/signals/stats.nim b/src/status/signals/stats.nim new file mode 100644 index 0000000..9522651 --- /dev/null +++ b/src/status/signals/stats.nim @@ -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 \ No newline at end of file diff --git a/src/status/signals/types.nim b/src/status/signals/types.nim new file mode 100644 index 0000000..c9f30e1 --- /dev/null +++ b/src/status/signals/types.nim @@ -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 \ No newline at end of file diff --git a/src/status/status.nim b/src/status/status.nim new file mode 100644 index 0000000..652d4e6 --- /dev/null +++ b/src/status/status.nim @@ -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() diff --git a/src/status/tasks/common.nim b/src/status/tasks/common.nim new file mode 100644 index 0000000..3b6b326 --- /dev/null +++ b/src/status/tasks/common.nim @@ -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) diff --git a/src/status/tasks/marathon.nim b/src/status/tasks/marathon.nim new file mode 100644 index 0000000..565cc42 --- /dev/null +++ b/src/status/tasks/marathon.nim @@ -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() diff --git a/src/status/tasks/marathon/common.nim b/src/status/tasks/marathon/common.nim new file mode 100644 index 0000000..f719742 --- /dev/null +++ b/src/status/tasks/marathon/common.nim @@ -0,0 +1,6 @@ +import # status-desktop libs + ../qt + +type + MarathonTaskArg* = ref object of QObjectTaskArg + `method`*: string diff --git a/src/status/tasks/marathon/worker.nim b/src/status/tasks/marathon/worker.nim new file mode 100644 index 0000000..1e40196 --- /dev/null +++ b/src/status/tasks/marathon/worker.nim @@ -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") \ No newline at end of file diff --git a/src/status/tasks/qt.nim b/src/status/tasks/qt.nim new file mode 100644 index 0000000..06d11ad --- /dev/null +++ b/src/status/tasks/qt.nim @@ -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) diff --git a/src/status/tasks/task_runner_impl.nim b/src/status/tasks/task_runner_impl.nim new file mode 100644 index 0000000..395da24 --- /dev/null +++ b/src/status/tasks/task_runner_impl.nim @@ -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() diff --git a/src/status/tasks/threadpool.nim b/src/status/tasks/threadpool.nim new file mode 100644 index 0000000..fcb614c --- /dev/null +++ b/src/status/tasks/threadpool.nim @@ -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.. 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) diff --git a/src/status/types.nim b/src/status/types.nim new file mode 100644 index 0000000..a1e9de1 --- /dev/null +++ b/src/status/types.nim @@ -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]") diff --git a/src/status/utils.nim b/src/status/utils.nim new file mode 100644 index 0000000..2b1252b --- /dev/null +++ b/src/status/utils.nim @@ -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)) diff --git a/src/status_node.nim b/src/status_node.nim index 77e38f7..ba6683b 100644 --- a/src/status_node.nim +++ b/src/status_node.nim @@ -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) diff --git a/ui/app/AppLayout.qml b/ui/app/AppLayout.qml new file mode 100644 index 0000000..3d30d43 --- /dev/null +++ b/ui/app/AppLayout.qml @@ -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)) + } + } +} \ No newline at end of file diff --git a/ui/app/BloomSelectorButton.qml b/ui/app/BloomSelectorButton.qml new file mode 100644 index 0000000..534dfc9 --- /dev/null +++ b/ui/app/BloomSelectorButton.qml @@ -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) + } + } +} diff --git a/ui/app/FleetRadioSelector.qml b/ui/app/FleetRadioSelector.qml new file mode 100644 index 0000000..f36f57d --- /dev/null +++ b/ui/app/FleetRadioSelector.qml @@ -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(); + } + } + } +} + diff --git a/ui/app/FleetsModal.qml b/ui/app/FleetsModal.qml new file mode 100644 index 0000000..50c6e5a --- /dev/null +++ b/ui/app/FleetsModal.qml @@ -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 + } + } +} diff --git a/ui/app/Rate.qml b/ui/app/Rate.qml new file mode 100644 index 0000000..b960c47 --- /dev/null +++ b/ui/app/Rate.qml @@ -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 + } + } + } +} \ No newline at end of file diff --git a/ui/app/img/caret.svg b/ui/app/img/caret.svg new file mode 100644 index 0000000..4339ce8 --- /dev/null +++ b/ui/app/img/caret.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/traffic_lights/close.png b/ui/app/img/traffic_lights/close.png new file mode 100644 index 0000000000000000000000000000000000000000..b310f2194021ea7a90d3e0752137b3dec6fb1b38 GIT binary patch literal 493 zcmV@~0drDELIAGL9O(c600d`2O+f$vv5yPy5)$eNWrH#RQab_@&hr z$A9{r9RK-!@w5FNL2I5JT`9uIwB?4bA0P@u>Nd-UOeOu*R;{nQJ+j>mhl$` zT(+!rhRX(rQYB02jG6XIib(~$B5)_2aH_@&Aqr~thwQ`&m-54W(*C%Qi7&j>z-2;E zxdKgn;8p{N34Gy3iEYh$waw3y^i==B5Rvds11}Q?+=#&;rtoPUBr1hWN_2?22CcW{ zzd9cy9SC=tupqdOxlWFG%M$C0XCZKNpv&#gMMzqq5P0*p_@)(#frpq-EZoP$RJ5cn znS{p}xKYPr?DrD+rAsCTvGASj^@K{M2iAwcnR39 jg*P=z`6#8|a>w`s<;5Q&zh{o800000NkvXXu0mjf8FSQE literal 0 HcmV?d00001 diff --git a/ui/app/img/traffic_lights/close_pressed.png b/ui/app/img/traffic_lights/close_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..fd0745e309e88181c2454190f37e69dc81ef2803 GIT binary patch literal 481 zcmV<70UrK|P)X1^@s6D=Y3@00009a7bBm001mY z001mY0i`{bsQ>@~0drDELIAGL9O(c600d`2O+f$vv5yPxS=I-S%-~Xv85}I#?u(l7y+fsL;csxABO&<-xJcSc zEO4DC@i?8vIlyrni49&N4)`^Xa4}e^h?kUZwrM7no;1N<()3E#xbi1Qi7UJ!aFOI$ zcH;^+3LGTHnBcVnUr8O@DsYw9;5r1Bd=$m!F3}g4Ct#l48#n&k#;^+XY7s)*aYH*b zL$-JVA#acm-hzfNbUBetLf-JQObky{D-k|$-BA8*kINXUF>Wj91D~&!Zl@FGdA>M9 z*0Qp#pf7wLR|&R(`$&F|OetS|;ZBmGI2GSg_S(XA^m#%=`aFpZj7ab$X$xGZx^prwfgF}}M_)$E)e-c@N{Fk0Cjv*C{Td(?ZH91He`&jOi?6JbK$^8S@ zy(PUDw$0v>d419BE4-iBZt|un?|E?ih=<_PXEk=*g01)E9VM97yH?K8V@XWdwM_G4 z&(rsh1L7Xl)G`Zb3K&Q$x*o}BYd9QUa(t5O?Ry7HgyLBx?F;O_SuxB$`=E@GHAauc z-{SVBf9)6lGr8{R+r@os$3^ap2Y=%^t>P0JR%Z%x z@mL(&&n)-B|A6p4b`7TQt*6hQg?;^nso j3z|6M=B{$OXV19&)xJAlgTe~DWM4f<0FOw literal 0 HcmV?d00001 diff --git a/ui/app/img/traffic_lights/maximize_pressed.png b/ui/app/img/traffic_lights/maximize_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..51ec389c80bace73ee4fef7bcd893b87b3447b6e GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9EO-XP4l)OOlRpde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N`~#jYjv*C{Tc5jgR3=A9lx&I`x0{O+BE{-7;j7KkR6m)P9V7;*abkCO(2iB+s z%#D&OCv=@y64J;u<$;xgG1IlcBX_yW{^fj{%x~d#|7G#ga}C;TLJ?1P>J;}4EfHEA*}}S3j3^P65jgR3=A9lx&I`x0{PjVE{-7;j7Kjx@;Mj?v_9Ogx~sIs>&P|H z9gVF=u6?v%^-IWK{C&cdX~r4*kDf^h)8|(bt}B@pqv@t&${2BDQg^A*j4Spb$IkWs zd-TWuP(`{#x!5B8OreubEQc 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 + } + } +} diff --git a/ui/shared/PopupMenu.qml b/ui/shared/PopupMenu.qml new file mode 100644 index 0000000..1921531 --- /dev/null +++ b/ui/shared/PopupMenu.qml @@ -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} +} +##^##*/ diff --git a/ui/shared/RoundedIcon.qml b/ui/shared/RoundedIcon.qml new file mode 100644 index 0000000..eed1d48 --- /dev/null +++ b/ui/shared/RoundedIcon.qml @@ -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} +} +##^##*/ diff --git a/ui/shared/RoundedImage.qml b/ui/shared/RoundedImage.qml new file mode 100644 index 0000000..e3b06ae --- /dev/null +++ b/ui/shared/RoundedImage.qml @@ -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() + } +} diff --git a/ui/shared/SVGImage.qml b/ui/shared/SVGImage.qml new file mode 100644 index 0000000..ec2d3b2 --- /dev/null +++ b/ui/shared/SVGImage.qml @@ -0,0 +1,9 @@ +import QtQuick 2.13 + +Image { + sourceSize.width: width || undefined + sourceSize.height: height || undefined + fillMode: Image.PreserveAspectFit + mipmap: true + antialiasing: true +} diff --git a/ui/shared/Separator.qml b/ui/shared/Separator.qml new file mode 100644 index 0000000..d21bcd8 --- /dev/null +++ b/ui/shared/Separator.qml @@ -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 + } +} diff --git a/ui/shared/SeparatorWithIcon.qml b/ui/shared/SeparatorWithIcon.qml new file mode 100644 index 0000000..3f98767 --- /dev/null +++ b/ui/shared/SeparatorWithIcon.qml @@ -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 + } +} diff --git a/ui/shared/StyledButton.qml b/ui/shared/StyledButton.qml new file mode 100644 index 0000000..65f8f05 --- /dev/null +++ b/ui/shared/StyledButton.qml @@ -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() + } + } +} diff --git a/ui/shared/StyledText.qml b/ui/shared/StyledText.qml new file mode 100644 index 0000000..e0286e5 --- /dev/null +++ b/ui/shared/StyledText.qml @@ -0,0 +1,7 @@ +import QtQuick 2.13 +import "../imports" + +Text { + font.family: Style.current.fontRegular.name + color: Style.current.textColor +} diff --git a/ui/shared/StyledTextEdit.qml b/ui/shared/StyledTextEdit.qml new file mode 100644 index 0000000..a3beb7d --- /dev/null +++ b/ui/shared/StyledTextEdit.qml @@ -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 +} diff --git a/ui/shared/StyledTextField.qml b/ui/shared/StyledTextField.qml new file mode 100644 index 0000000..0059742 --- /dev/null +++ b/ui/shared/StyledTextField.qml @@ -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 +} diff --git a/ui/shared/TertiaryButton.qml b/ui/shared/TertiaryButton.qml new file mode 100644 index 0000000..0ee66ec --- /dev/null +++ b/ui/shared/TertiaryButton.qml @@ -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() + } + } +} \ No newline at end of file diff --git a/ui/shared/TextWithLabel.qml b/ui/shared/TextWithLabel.qml new file mode 100644 index 0000000..8384566 --- /dev/null +++ b/ui/shared/TextWithLabel.qml @@ -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} +} +##^##*/ diff --git a/ui/shared/Timer.qml b/ui/shared/Timer.qml new file mode 100644 index 0000000..e9e0edc --- /dev/null +++ b/ui/shared/Timer.qml @@ -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(); + } +} \ No newline at end of file diff --git a/ui/shared/img/close.svg b/ui/shared/img/close.svg new file mode 100644 index 0000000..b871e5d --- /dev/null +++ b/ui/shared/img/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/shared/qmldir b/ui/shared/qmldir new file mode 100644 index 0000000..fa70283 --- /dev/null +++ b/ui/shared/qmldir @@ -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 diff --git a/ui/shared/status/StatusButton.qml b/ui/shared/status/StatusButton.qml new file mode 100644 index 0000000..97f2f0f --- /dev/null +++ b/ui/shared/status/StatusButton.qml @@ -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 + } +} + diff --git a/ui/shared/status/StatusCheckBox.qml b/ui/shared/status/StatusCheckBox.qml new file mode 100644 index 0000000..fce3ef5 --- /dev/null +++ b/ui/shared/status/StatusCheckBox.qml @@ -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 + } +} + diff --git a/ui/shared/status/StatusContextMenuButton.qml b/ui/shared/status/StatusContextMenuButton.qml new file mode 100644 index 0000000..8461588 --- /dev/null +++ b/ui/shared/status/StatusContextMenuButton.qml @@ -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 +} diff --git a/ui/shared/status/StatusRadioButton.qml b/ui/shared/status/StatusRadioButton.qml new file mode 100644 index 0000000..4d3b818 --- /dev/null +++ b/ui/shared/status/StatusRadioButton.qml @@ -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 + } +} diff --git a/ui/shared/status/StatusRadioButtonRow.qml b/ui/shared/status/StatusRadioButtonRow.qml new file mode 100644 index 0000000..1556bc2 --- /dev/null +++ b/ui/shared/status/StatusRadioButtonRow.qml @@ -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 + } + } + +} diff --git a/ui/shared/status/StatusRoundButton.qml b/ui/shared/status/StatusRoundButton.qml new file mode 100644 index 0000000..7876e18 --- /dev/null +++ b/ui/shared/status/StatusRoundButton.qml @@ -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 + } +} diff --git a/ui/shared/status/StatusSectionHeadline.qml b/ui/shared/status/StatusSectionHeadline.qml new file mode 100644 index 0000000..1388789 --- /dev/null +++ b/ui/shared/status/StatusSectionHeadline.qml @@ -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 +} + diff --git a/ui/shared/status/StatusSettingsLineButton.qml b/ui/shared/status/StatusSettingsLineButton.qml new file mode 100644 index 0000000..a00deee --- /dev/null +++ b/ui/shared/status/StatusSettingsLineButton.qml @@ -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 + } +} diff --git a/ui/shared/status/StatusSwitch.qml b/ui/shared/status/StatusSwitch.qml new file mode 100644 index 0000000..047eadc --- /dev/null +++ b/ui/shared/status/StatusSwitch.qml @@ -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 + } +} + diff --git a/ui/shared/status/core/StatusIcon.qml b/ui/shared/status/core/StatusIcon.qml new file mode 100644 index 0000000..83d4f5e --- /dev/null +++ b/ui/shared/status/core/StatusIcon.qml @@ -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 + } +} + diff --git a/ui/shared/status/core/StatusLoadingIndicator.qml b/ui/shared/status/core/StatusLoadingIndicator.qml new file mode 100644 index 0000000..9cd3d35 --- /dev/null +++ b/ui/shared/status/core/StatusLoadingIndicator.qml @@ -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 + } +} diff --git a/ui/shared/status/qmldir b/ui/shared/status/qmldir new file mode 100644 index 0000000..8ed1818 --- /dev/null +++ b/ui/shared/status/qmldir @@ -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 diff --git a/vendor/nim-confutils b/vendor/nim-confutils new file mode 160000 index 0000000..ab4ba1c --- /dev/null +++ b/vendor/nim-confutils @@ -0,0 +1 @@ +Subproject commit ab4ba1cbfdccdb8c0398894ffc25169bc61faeed diff --git a/vendor/nim-status-go b/vendor/nim-status-go index 2b6e504..5d20a34 160000 --- a/vendor/nim-status-go +++ b/vendor/nim-status-go @@ -1 +1 @@ -Subproject commit 2b6e50491786ae0d61a97f99edda27b70364838a +Subproject commit 5d20a34714d1e4df286eb423e5447adc955bcffa diff --git a/vendor/status-go b/vendor/status-go index 0e8c7ee..83c1e3c 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 0e8c7eef73427c78c413637c74ba8c1031fdb20c +Subproject commit 83c1e3c84b02665838fd4b3aea91f4e80fc887fb