diff --git a/storybook/CMakeLists.txt b/storybook/CMakeLists.txt
index 155e1afaa61..94a48bf6dc1 100644
--- a/storybook/CMakeLists.txt
+++ b/storybook/CMakeLists.txt
@@ -76,7 +76,7 @@ target_link_libraries(
${PROJECT_LIB} PUBLIC Qt5::Core Qt5::Gui Qt5::Quick Qt5::QuickControls2 Qt5::WebView)
target_link_libraries(
- ${PROJECT_NAME} PRIVATE ${PROJECT_LIB})
+ ${PROJECT_NAME} PRIVATE ${PROJECT_LIB})
add_dependencies(${PROJECT_NAME} StatusQ)
@@ -123,6 +123,7 @@ add_test(NAME QmlTests COMMAND QmlTests -platform offscreen)
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/imports")
+list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/StatusQ/src")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/src")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/pages")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/stubs")
diff --git a/storybook/pages/ManageTokensPanelPage.qml b/storybook/pages/ManageTokensPanelPage.qml
new file mode 100644
index 00000000000..88600b1f4f2
--- /dev/null
+++ b/storybook/pages/ManageTokensPanelPage.qml
@@ -0,0 +1,87 @@
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import StatusQ.Core 0.1
+
+import AppLayouts.Wallet.panels 1.0
+
+import utils 1.0
+
+import Storybook 1.0
+import Models 1.0
+
+SplitView {
+ id: root
+
+ Logs { id: logs }
+
+ orientation: Qt.Horizontal
+
+ ManageTokensModel {
+ id: assetsModel
+ }
+
+ StatusScrollView { // wrapped in a ScrollView on purpose; to simulate SettingsContentBase.qml
+ SplitView.fillWidth: true
+ SplitView.preferredHeight: 500
+ ManageTokensPanel {
+ id: showcasePanel
+ width: 500
+ baseModel: ctrlEmptyModel.checked ? null : assetsModel
+ }
+ }
+
+ LogsAndControlsPanel {
+ id: logsAndControlsPanel
+
+ SplitView.minimumWidth: 150
+ SplitView.preferredWidth: 250
+
+ logsView.logText: logs.logText
+
+ ColumnLayout {
+ Label {
+ Layout.fillWidth: true
+ text: "Dirty: %1".arg(showcasePanel.dirty ? "true" : "false")
+ }
+
+ Button {
+ text: "Save"
+ onClicked: showcasePanel.saveSettings()
+ }
+
+ Button {
+ enabled: showcasePanel.dirty
+ text: "Revert"
+ onClicked: showcasePanel.revert()
+ }
+
+ Button {
+ text: "Random data"
+ onClicked: {
+ assetsModel.clear()
+ assetsModel.randomizeData()
+ }
+ }
+
+ Button {
+ text: "Clear settings"
+ onClicked: showcasePanel.clearSettings()
+ }
+
+ Switch {
+ id: ctrlEmptyModel
+ text: "Empty model"
+ }
+ }
+ }
+}
+
+// category: Panels
+
+// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=18139-95033&mode=design&t=nqFScWLfusXBNQA5-0
+// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17674-273051&mode=design&t=nqFScWLfusXBNQA5-0
+// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17636-249780&mode=design&t=nqFScWLfusXBNQA5-0
+// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17674-276833&mode=design&t=nqFScWLfusXBNQA5-0
+// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17675-283206&mode=design&t=nqFScWLfusXBNQA5-0
diff --git a/storybook/src/Models/ManageTokensModel.qml b/storybook/src/Models/ManageTokensModel.qml
new file mode 100644
index 00000000000..cb5bb7c556a
--- /dev/null
+++ b/storybook/src/Models/ManageTokensModel.qml
@@ -0,0 +1,237 @@
+import QtQuick 2.15
+import QtQml.Models 2.15
+
+import Models 1.0
+
+ListModel {
+ function randomizeData() {
+ var data = []
+ for (let i = 0; i < 100; i++) {
+ const communityId = i % 2 == 0 ? "" : "communityId%1".arg(Math.round(i))
+ const enabledNetworkBalance = !!communityId ? Math.round(i)
+ : {
+ amount: 1,
+ symbol: "ZRX"
+ }
+ var obj = {
+ name: "Item %1".arg(i),
+ symbol: "SYM %1".arg(i),
+ enabledNetworkBalance: enabledNetworkBalance,
+ enabledNetworkCurrencyBalance: {
+ amount: 10.37,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ communityId: communityId,
+ communityName: "COM %1".arg(i),
+ communityImage: ""
+ }
+ data.push(obj)
+ }
+ append(data)
+ }
+
+ readonly property var data: [
+ {
+ name: "0x",
+ symbol: "ZRX",
+ enabledNetworkBalance: {
+ amount: 1,
+ symbol: "ZRX"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 10.37,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ communityId: "ddls",
+ communityName: "Doodles",
+ communityImage: ModelsData.collectibles.doodles // FIXME backend
+ },
+ {
+ name: "Omg",
+ symbol: "OMG",
+ enabledNetworkBalance: {
+ amount: 2,
+ symbol: "OMG"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 13.37,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ communityId: "sox",
+ communityName: "Socks",
+ communityImage: ModelsData.icons.socks
+ },
+ {
+ name: "Decentraland",
+ symbol: "MANA",
+ enabledNetworkBalance: {
+ amount: 301,
+ symbol: "MANA"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 75.256,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: -2.1,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Ave Maria",
+ symbol: "AAVE",
+ enabledNetworkBalance: {
+ amount: 23.3,
+ symbol: "AAVE",
+ displayDecimals: 2
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 2.335,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: 4.56,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Polymorphism",
+ symbol: "POLY",
+ enabledNetworkBalance: {
+ amount: 3590,
+ symbol: "POLY"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 2.7,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: -11.6789,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Dai",
+ symbol: "DAI",
+ enabledNetworkBalance: {
+ amount: 634.22,
+ symbol: "DAI",
+ displayDecimals: 2
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 594.72,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: 0,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Makers' choice",
+ symbol: "MKR",
+ enabledNetworkBalance: {
+ amount: 1.3,
+ symbol: "MKR",
+ displayDecimals: 2
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 100.37,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: -1,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Ethereum",
+ symbol: "ETH",
+ enabledNetworkBalance: {
+ amount: 0.12345,
+ symbol: "ETH",
+ displayDecimals: 8,
+ stripTrailingZeroes: true
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 182.72,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ changePct24hour: -3.51,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "GetOuttaHere",
+ symbol: "InvisibleSYM",
+ enabledNetworkBalance: {},
+ enabledNetworkCurrencyBalance: {},
+ changePct24hour: NaN,
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ enabledNetworkBalance: ({
+ displayDecimals: true,
+ stripTrailingZeroes: true,
+ amount: 324343.3,
+ symbol: "SNT"
+ }),
+ enabledNetworkCurrencyBalance: ({
+ displayDecimals: 4,
+ stripTrailingZeroes: true,
+ amount: 2.333321323400,
+ symbol: "EUR"
+ }),
+ symbol: "SNT",
+ name: "Status",
+ communityId: "",
+ communityName: "",
+ communityImage: ""
+ },
+ {
+ name: "Meth",
+ symbol: "MET",
+ enabledNetworkBalance: {
+ amount: 666,
+ symbol: "MET"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 1000.37,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ communityId: "ddls",
+ communityName: "Doodles",
+ communityImage: ModelsData.collectibles.doodles
+ },
+ {
+ name: "Ast",
+ symbol: "AST",
+ enabledNetworkBalance: {
+ amount: 1,
+ symbol: "AST"
+ },
+ enabledNetworkCurrencyBalance: {
+ amount: 0.374,
+ symbol: "EUR",
+ displayDecimals: 2
+ },
+ communityId: "ast",
+ communityName: "Astafarians",
+ communityImage: ModelsData.icons.dribble
+ }
+ ]
+ Component.onCompleted: append(data)
+}
diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir
index 81d973c0c8b..79023fac7e5 100644
--- a/storybook/src/Models/qmldir
+++ b/storybook/src/Models/qmldir
@@ -9,6 +9,7 @@ FlatTokensModel 1.0 FlatTokensModel.qml
IconModel 1.0 IconModel.qml
LinkPreviewModel 1.0 LinkPreviewModel.qml
MintedTokensModel 1.0 MintedTokensModel.qml
+ManageTokensModel 1.0 ManageTokensModel.qml
RecipientModel 1.0 RecipientModel.qml
SourceOfTokensModel 1.0 SourceOfTokensModel.qml
TokenHoldersModel 1.0 TokenHoldersModel.qml
diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt
index c42422abfeb..eefb2adbe61 100644
--- a/ui/StatusQ/CMakeLists.txt
+++ b/ui/StatusQ/CMakeLists.txt
@@ -109,6 +109,12 @@ add_library(StatusQ SHARED
src/statuswindow.cpp
src/stringutilsinternal.cpp
src/submodelproxymodel.cpp
+
+ # wallet
+ src/wallet/managetokenscontroller.cpp
+ src/wallet/managetokenscontroller.h
+ src/wallet/managetokensmodel.cpp
+ src/wallet/managetokensmodel.h
)
set_target_properties(StatusQ PROPERTIES
diff --git a/ui/StatusQ/src/StatusQ/Components/StatusDraggableListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusDraggableListItem.qml
index f51790b69ee..df0a5e15ec5 100644
--- a/ui/StatusQ/src/StatusQ/Components/StatusDraggableListItem.qml
+++ b/ui/StatusQ/src/StatusQ/Components/StatusDraggableListItem.qml
@@ -146,9 +146,14 @@ ItemDelegate {
property int visualIndex
/*!
\qmlproperty bool StatusDraggableListItem::draggable
- This property holds whether this item can be dragged (and whether the drag handle is displayed)
+ This property holds whether the drag handle is displayed
*/
property bool draggable
+ /*!
+ \qmlproperty bool StatusDraggableListItem::dragEnabled
+ This property holds whether this item can be dragged (and whether the drag handle is displayed)
+ */
+ property bool dragEnabled: draggable
/*!
\qmlproperty bool StatusDraggableListItem::customizable
This property holds whether this item can be customized
@@ -200,6 +205,13 @@ ItemDelegate {
*/
property color bgColor: "transparent"
+ /*!
+ \qmlproperty color StatusDraggableListItem::assetBgColor
+ This property holds icon/image background color, if any
+ Defaults to "transparent" (ie no background)
+ */
+ property color assetBgColor: "transparent"
+
Drag.dragType: Drag.Automatic
Drag.hotSpot.x: dragHandler.mouseX
Drag.hotSpot.y: dragHandler.mouseY
@@ -209,7 +221,7 @@ ItemDelegate {
\qmlproperty readonly bool StatusDraggableListItem::dragActive
This property holds whether a drag is currently in progress
*/
- readonly property bool dragActive: draggable && dragHandler.drag.active
+ readonly property bool dragActive: dragHandler.drag.active
onDragActiveChanged: {
if (dragActive)
Drag.start()
@@ -234,19 +246,25 @@ ItemDelegate {
]
background: Rectangle {
- color: root.dragActive && !root.customizable ? Theme.palette.indirectColor2 : "transparent"
+ color: root.dragActive && !root.customizable ? Theme.palette.alphaColor(Theme.palette.baseColor2, 0.7) : root.bgColor
border.width: root.customizable ? 0 : 1
border.color: Theme.palette.baseColor2
- radius: customizable ? 0 : 8
+ radius: root.customizable ? 0 : 8
MouseArea {
id: dragHandler
anchors.fill: parent
- drag.target: root.draggable ? root : null
+ drag.target: root.dragEnabled ? root : null
drag.axis: root.dragAxis
preventStealing: true // otherwise DND is broken inside a Flickable/ScrollView
hoverEnabled: true
- cursorShape: root.dragActive ? Qt.ClosedHandCursor : Qt.OpenHandCursor
+ cursorShape: {
+ if (!root.enabled)
+ return undefined
+ if (root.dragEnabled)
+ return root.dragActive ? Qt.ClosedHandCursor : Qt.OpenHandCursor
+ return Qt.PointingHandCursor
+ }
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.clicked(mouse);
@@ -255,8 +273,8 @@ ItemDelegate {
}
// inset to simulate spacing
- topInset: 6
- bottomInset: 6
+ topInset: 4
+ bottomInset: 4
horizontalPadding: 12
verticalPadding: 16
@@ -273,10 +291,10 @@ ItemDelegate {
Layout.preferredHeight: 20
icon: "justify"
visible: root.draggable && !root.customizable
+ color: root.dragEnabled ? Theme.palette.baseColor1 : Theme.palette.baseColor2
}
Loader {
- Layout.leftMargin: root.spacing/2
asynchronous: true
active: !!root.icon.name || !!root.icon.source
visible: active
@@ -293,7 +311,7 @@ ItemDelegate {
visible: text
elide: Text.ElideRight
maximumLineCount: 1
- font.weight: root.highlighted ? Font.Medium : Font.Normal
+ font.weight: Font.Medium
}
Row {
@@ -302,6 +320,8 @@ ItemDelegate {
spacing: 8
StatusBaseText {
+ width: Math.min(parent.width - (secondaryTitleIconLoader.item ? parent.spacing + secondaryTitleIconLoader.item.width : 0),
+ implicitWidth)
text: root.secondaryTitle
color: Theme.palette.baseColor1
elide: Text.ElideRight
@@ -309,6 +329,8 @@ ItemDelegate {
}
Loader {
+ id: secondaryTitleIconLoader
+ anchors.verticalCenter: parent.verticalCenter
asynchronous: true
active: !!root.secondaryTitleIcon
visible: active
@@ -349,13 +371,10 @@ ItemDelegate {
id: imageComponent
StatusRoundedImage {
radius: root.bgRadius
- color: root.bgColor
+ color: root.assetBgColor
width: root.icon.width
height: root.icon.height
image.source: root.icon.source
- image.sourceSize: Qt.size(width, height)
- image.smooth: false
- image.mipmap: true
showLoadingIndicator: true
image.fillMode: Image.PreserveAspectCrop
}
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
index b8a7ecf7616..18c62212454 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
@@ -74,7 +74,6 @@ CheckBox {
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
width: parent.width
- color: Theme.palette.directColor1
lineHeight: 1.2
leftPadding: root.leftSide? (!!root.text ? root.indicator.width + root.spacing
: root.indicator.width) : 0
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml b/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml
index 89eebbf50e6..90c104032f7 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml
@@ -18,7 +18,10 @@ ItemDelegate {
icon.width: 16
icon.height: 16
- contentItem: RowLayout {
+ font.family: Theme.palette.baseFont.name
+ font.pixelSize: 15
+
+ contentItem: RowLayout {
spacing: root.spacing
StatusIcon {
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusSwitch.qml b/ui/StatusQ/src/StatusQ/Controls/StatusSwitch.qml
index 11acd6b9f8a..5daef05b8c1 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusSwitch.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusSwitch.qml
@@ -8,6 +8,8 @@ import StatusQ.Components 0.1
Switch {
id: root
+ property color textColor: Theme.palette.directColor1
+
background: MouseArea {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
@@ -18,8 +20,9 @@ Switch {
implicitWidth: 52
implicitHeight: 28
- x: root.leftPadding
- y: parent.height / 2 - height / 2
+ anchors.left: parent.left
+ anchors.leftMargin: root.leftPadding
+ anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
@@ -71,8 +74,9 @@ Switch {
contentItem: StatusBaseText {
text: root.text
opacity: enabled ? 1.0 : 0.3
+ color: root.textColor
verticalAlignment: Text.AlignVCenter
- leftPadding: !!root.text ? root.indicator.width + root.spacing
- : root.indicator.width
+ leftPadding: root.mirrored ? 0 : !!root.text ? root.indicator.width + root.spacing : root.indicator.width
+ rightPadding: root.mirrored ? !!root.text ? root.indicator.width + root.spacing : root.indicator.width : 0
}
}
diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusAction.qml b/ui/StatusQ/src/StatusQ/Popups/StatusAction.qml
index b1c099fd0d1..460e9216e54 100644
--- a/ui/StatusQ/src/StatusQ/Popups/StatusAction.qml
+++ b/ui/StatusQ/src/StatusQ/Popups/StatusAction.qml
@@ -23,6 +23,7 @@ Action {
imgIsIdenticon: false
color: root.icon.color
name: root.icon.name
+ hoverColor: Theme.palette.statusMenu.hoverBackgroundColor
}
property StatusFontSettings fontSettings: StatusFontSettings {}
diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusMenu.qml b/ui/StatusQ/src/StatusQ/Popups/StatusMenu.qml
index 0bde374dac0..a283dd94d70 100644
--- a/ui/StatusQ/src/StatusQ/Popups/StatusMenu.qml
+++ b/ui/StatusQ/src/StatusQ/Popups/StatusMenu.qml
@@ -36,13 +36,23 @@ Menu {
property real maxImplicitWidth: 640
readonly property color defaultIconColor: Theme.palette.primaryColor1
+ property int type: StatusAction.Type.Normal
+
property StatusAssetSettings assetSettings: StatusAssetSettings {
width: 18
height: 18
rotation: 0
isLetterIdenticon: false
isImage: false
- color: root.defaultIconColor
+ color: {
+ if (!root.enabled)
+ return Theme.palette.baseColor1
+ if (root.type === StatusAction.Type.Danger)
+ return Theme.palette.dangerColor1
+ if (root.type === StatusAction.Type.Success)
+ return Theme.palette.successColor1
+ return Theme.palette.primaryColor1
+ }
}
property StatusFontSettings fontSettings: StatusFontSettings {}
@@ -57,8 +67,6 @@ Menu {
property var openHandler
property var closeHandler
- signal menuItemClicked(int menuIndex)
-
function checkIfEmpty() {
for (let i = 0; i < root.contentItem.count; ++i) {
const menuItem = root.contentItem.itemAtIndex(i)
@@ -98,7 +106,8 @@ Menu {
visible: root.hideDisabledItems ? enabled : true
height: visible ? implicitHeight : 0
onImplicitWidthChanged: {
- d.maxDelegateImplWidth = Math.max(d.maxDelegateImplWidth, implicitWidth)
+ if (visible)
+ d.maxDelegateImplWidth = Math.max(d.maxDelegateImplWidth, implicitWidth)
}
}
diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusMenuItem.qml b/ui/StatusQ/src/StatusQ/Popups/StatusMenuItem.qml
index 2d02aa72c23..9dc0f278c0c 100644
--- a/ui/StatusQ/src/StatusQ/Popups/StatusMenuItem.qml
+++ b/ui/StatusQ/src/StatusQ/Popups/StatusMenuItem.qml
@@ -23,8 +23,10 @@ MenuItem {
readonly property bool subMenuOpened: isSubMenu && root.subMenu.opened
readonly property bool hasAction: !!root.action
readonly property bool isStatusAction: d.hasAction && (root.action instanceof StatusAction)
- readonly property bool isStatusDangerAction: d.isStatusAction && root.action.type === StatusAction.Type.Danger
- readonly property bool isStatusSuccessAction: d.isStatusAction && root.action.type === StatusAction.Type.Success
+ readonly property bool isStatusDangerAction: (d.isStatusAction && root.action.type === StatusAction.Type.Danger) ||
+ (d.isStatusSubMenu && root.subMenu.type === StatusAction.Type.Danger)
+ readonly property bool isStatusSuccessAction: (d.isStatusAction && root.action.type === StatusAction.Type.Success) ||
+ (d.isStatusSubMenu && root.subMenu.type === StatusAction.Type.Success)
readonly property StatusAssetSettings originalAssetSettings: d.isStatusSubMenu && root.subMenu.assetSettings
? root.subMenu.assetSettings
diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusSearchPopupMenuItem.qml b/ui/StatusQ/src/StatusQ/Popups/StatusSearchPopupMenuItem.qml
index 4539e6c2cbd..1e02a42bd00 100644
--- a/ui/StatusQ/src/StatusQ/Popups/StatusSearchPopupMenuItem.qml
+++ b/ui/StatusQ/src/StatusQ/Popups/StatusSearchPopupMenuItem.qml
@@ -1,10 +1,6 @@
import QtQuick 2.14
-import QtQuick.Controls 2.14
-import QtQuick.Layouts 1.12
-import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
-import StatusQ.Controls 0.1
StatusAction {
id: root
diff --git a/ui/StatusQ/src/assets/img/icons/arrow-bottom.svg b/ui/StatusQ/src/assets/img/icons/arrow-bottom.svg
new file mode 100644
index 00000000000..4a0b3a81883
--- /dev/null
+++ b/ui/StatusQ/src/assets/img/icons/arrow-bottom.svg
@@ -0,0 +1,3 @@
+
diff --git a/ui/StatusQ/src/assets/img/icons/arrow-top.svg b/ui/StatusQ/src/assets/img/icons/arrow-top.svg
new file mode 100644
index 00000000000..d0a23f52b1a
--- /dev/null
+++ b/ui/StatusQ/src/assets/img/icons/arrow-top.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp
index 82a073ad2e2..494dcccdc39 100644
--- a/ui/StatusQ/src/plugin.cpp
+++ b/ui/StatusQ/src/plugin.cpp
@@ -15,6 +15,9 @@
#include "StatusQ/submodelproxymodel.h"
+#include "wallet/managetokenscontroller.h"
+#include "wallet/managetokensmodel.h"
+
class StatusQPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
@@ -28,6 +31,9 @@ class StatusQPlugin : public QQmlExtensionPlugin
qmlRegisterType("StatusQ", 0, 1, "StatusSyntaxHighlighter");
qmlRegisterType("StatusQ", 0, 1, "RXValidator");
+ qmlRegisterType("StatusQ.Models", 0, 1, "ManageTokensController");
+ qmlRegisterType("StatusQ.Models", 0, 1, "ManageTokensModel");
+
qmlRegisterType("StatusQ", 0, 1, "LeftJoinModel");
qmlRegisterType("StatusQ", 0, 1, "SubmodelProxyModel");
qmlRegisterType("StatusQ", 0, 1, "RoleRename");
diff --git a/ui/StatusQ/src/wallet/managetokenscontroller.cpp b/ui/StatusQ/src/wallet/managetokenscontroller.cpp
new file mode 100644
index 00000000000..9691a630c9e
--- /dev/null
+++ b/ui/StatusQ/src/wallet/managetokenscontroller.cpp
@@ -0,0 +1,393 @@
+#include "managetokenscontroller.h"
+
+#include
+
+ManageTokensController::ManageTokensController(QObject* parent)
+ : QObject(parent)
+ , m_regularTokensModel(new ManageTokensModel(this))
+ , m_communityTokensModel(new ManageTokensModel(this))
+ , m_communityTokenGroupsModel(new ManageTokensModel(this))
+ , m_hiddenTokensModel(new ManageTokensModel(this))
+{
+ for (auto model : m_allModels) {
+ connect(model, &ManageTokensModel::dirtyChanged, this, &ManageTokensController::dirtyChanged);
+ }
+
+ connect(this, &ManageTokensController::sourceModelChanged, this, [this]() {
+ if (!m_sourceModel) {
+ m_modelConnectionsInitialized = false;
+ return;
+ }
+ if (m_modelConnectionsInitialized)
+ return;
+ connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
+#ifdef QT_DEBUG
+ QElapsedTimer t;
+ t.start();
+ qCInfo(manageTokens) << "!!! ADDING" << last-first+1 << "NEW TOKENS";
+#endif
+ for (int i = first; i <= last; i++)
+ addItem(i);
+ reloadCommunityIds();
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+ m_communityTokensModel->saveCustomSortOrder();
+ rebuildCommunityTokenGroupsModel();
+#ifdef QT_DEBUG
+ qCInfo(manageTokens) << "!!! ADDING NEW SOURCE DATA TOOK" << t.nsecsElapsed()/1'000'000.f << "ms";
+#endif
+ });
+ connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &ManageTokensController::parseSourceModel);
+ connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &ManageTokensController::parseSourceModel); // NB at this point we don't know in which submodel the item is
+ connect(m_communityTokensModel, &ManageTokensModel::rowsMoved, this, [this]() {
+ if (!m_arrangeByCommunity)
+ rebuildCommunityTokenGroupsModel();
+ reloadCommunityIds();
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+ m_communityTokensModel->saveCustomSortOrder();
+ });
+ connect(m_communityTokenGroupsModel, &ManageTokensModel::rowsMoved, this, [this](const QModelIndex &parent, int start, int end, const QModelIndex &destination, int toRow) {
+ qCDebug(manageTokens) << "!!! GROUP MOVED FROM" << start << "TO" << toRow;
+ // FIXME swap toRow<->start instead of reloadCommunityIds()?
+ reloadCommunityIds();
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+ m_communityTokensModel->saveCustomSortOrder();
+ });
+ m_modelConnectionsInitialized = true;
+ });
+}
+
+void ManageTokensController::showHideRegularToken(int row, bool flag)
+{
+ if (flag) { // show
+ auto hiddenItem = m_hiddenTokensModel->takeItem(row);
+ if (hiddenItem)
+ m_regularTokensModel->addItem(*hiddenItem);
+ } else { // hide
+ auto shownItem = m_regularTokensModel->takeItem(row);
+ if (shownItem)
+ m_hiddenTokensModel->addItem(*shownItem, false /*prepend*/);
+ }
+}
+
+void ManageTokensController::showHideCommunityToken(int row, bool flag)
+{
+ if (flag) { // show
+ auto hiddenItem = m_hiddenTokensModel->takeItem(row);
+ if (hiddenItem) {
+ m_communityTokensModel->addItem(*hiddenItem);
+ if (!m_communityIds.contains(hiddenItem->communityId))
+ m_communityIds.append(hiddenItem->communityId);
+ }
+ } else { // hide
+ auto shownItem = m_communityTokensModel->takeItem(row);
+ if (shownItem) {
+ m_hiddenTokensModel->addItem(*shownItem, false /*prepend*/);
+ if (!m_communityTokensModel->hasCommunityIdToken(shownItem->communityId))
+ m_communityIds.removeAll(shownItem->communityId);
+ }
+ }
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+ m_communityTokensModel->saveCustomSortOrder();
+ rebuildCommunityTokenGroupsModel();
+}
+
+void ManageTokensController::showHideGroup(const QString& groupId, bool flag)
+{
+ if (flag) { // show
+ const auto tokens = m_hiddenTokensModel->takeAllItems(groupId);
+ for (const auto& token: tokens) {
+ m_communityTokensModel->addItem(token);
+ }
+ m_communityIds.append(groupId);
+ } else { // hide
+ const auto tokens = m_communityTokensModel->takeAllItems(groupId);
+ for (const auto& token: tokens) {
+ m_hiddenTokensModel->addItem(token, false /*prepend*/);
+ }
+ m_communityIds.removeAll(groupId);
+ }
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+ m_communityTokensModel->saveCustomSortOrder();
+ rebuildCommunityTokenGroupsModel();
+}
+
+void ManageTokensController::saveSettings()
+{
+ Q_ASSERT(!m_settingsKey.isEmpty());
+
+ // gather the data to save
+ SerializedTokenData result;
+ for (auto model: {m_regularTokensModel, m_communityTokensModel})
+ result.insert(model->save());
+ result.insert(m_hiddenTokensModel->save(false));
+
+ // save to QSettings
+ m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
+ m_settings.beginWriteArray(m_settingsKey);
+ SerializedTokenData::const_key_value_iterator it = result.constKeyValueBegin();
+ for (auto i = 0; it != result.constKeyValueEnd() && i < result.size(); it++, i++) {
+ m_settings.setArrayIndex(i);
+ const auto tuple = it->second;
+ m_settings.setValue(QStringLiteral("symbol"), it->first);
+ m_settings.setValue(QStringLiteral("pos"), std::get<0>(tuple));
+ m_settings.setValue(QStringLiteral("visible"), std::get<1>(tuple));
+ m_settings.setValue(QStringLiteral("groupId"), std::get<2>(tuple));
+ }
+ m_settings.endArray();
+ m_settings.endGroup();
+ m_settings.sync();
+
+ // unset dirty
+ for (auto model: m_allModels)
+ model->setDirty(false);
+}
+
+void ManageTokensController::clearSettings()
+{
+ Q_ASSERT(!m_settingsKey.isEmpty());
+
+ // clear the relevant QSettings group
+ m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
+ m_settings.remove(QString());
+ m_settings.endGroup();
+ m_settings.sync();
+}
+
+void ManageTokensController::loadSettings()
+{
+ Q_ASSERT(!m_settingsKey.isEmpty());
+
+ m_settingsData.clear();
+
+ // load from QSettings
+ m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
+ const auto size = m_settings.beginReadArray(m_settingsKey);
+ for (auto i = 0; i < size; i++) {
+ m_settings.setArrayIndex(i);
+ const auto symbol = m_settings.value(QStringLiteral("symbol")).toString();
+ if (symbol.isEmpty()) {
+ qCWarning(manageTokens) << Q_FUNC_INFO << "Missing symbol while reading tokens settings";
+ continue;
+ }
+ const auto pos = m_settings.value(QStringLiteral("pos"), -1).toInt();
+ const auto visible = m_settings.value(QStringLiteral("visible"), true).toBool();
+ const auto groupId = m_settings.value(QStringLiteral("groupId")).toString();
+ m_settingsData.insert(symbol, {pos, visible, groupId});
+ }
+ m_settings.endArray();
+ m_settings.endGroup();
+}
+
+void ManageTokensController::revert()
+{
+ loadSettings();
+ parseSourceModel();
+}
+
+void ManageTokensController::classBegin()
+{
+ // empty on purpose
+}
+
+void ManageTokensController::componentComplete()
+{
+ loadSettings();
+}
+
+void ManageTokensController::setSourceModel(QAbstractItemModel* newSourceModel)
+{
+ if(m_sourceModel == newSourceModel) return;
+
+ if(!newSourceModel) {
+ disconnect(sourceModel());
+ // clear all the models
+ for (auto model: m_allModels)
+ model->clear();
+ m_communityIds.clear();
+ m_sourceModel = newSourceModel;
+ emit sourceModelChanged();
+ return;
+ }
+
+ m_sourceModel = newSourceModel;
+
+ connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &ManageTokensController::parseSourceModel);
+
+ if (m_sourceModel && m_sourceModel->roleNames().isEmpty()) { // workaround for when a model has no roles and roles are added when the model is populated (ListModel)
+ // QTBUG-57971
+ connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ManageTokensController::parseSourceModel);
+ return;
+ } else {
+ parseSourceModel();
+ }
+}
+
+void ManageTokensController::parseSourceModel()
+{
+ if (!m_sourceModel)
+ return;
+
+ disconnect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ManageTokensController::parseSourceModel);
+
+#ifdef QT_DEBUG
+ QElapsedTimer t;
+ t.start();
+#endif
+
+ // clear all the models
+ for (auto model: m_allModels)
+ model->clear();
+ m_communityIds.clear();
+
+ // read and transform the original data
+ const auto newSize = m_sourceModel->rowCount();
+ qCInfo(manageTokens) << "!!! PARSING" << newSize << "TOKENS";
+ for (auto i = 0; i < newSize; i++) {
+ addItem(i);
+ }
+
+ // build community groups model
+ rebuildCommunityTokenGroupsModel();
+ reloadCommunityIds();
+ m_communityTokensModel->setCommunityIds(m_communityIds);
+
+ // (pre)sort
+ for (auto model: m_allModels) {
+ model->applySort();
+ model->saveCustomSortOrder();
+ model->setDirty(false);
+ }
+
+#ifdef QT_DEBUG
+ qCInfo(manageTokens) << "!!! PARSING SOURCE DATA TOOK" << t.nsecsElapsed()/1'000'000.f << "ms";
+#endif
+
+ emit sourceModelChanged();
+}
+
+void ManageTokensController::addItem(int index)
+{
+ const auto sourceRoleNames = m_sourceModel->roleNames();
+
+ const auto dataForIndex = [&](const QModelIndex &idx, const QByteArray& rolename) -> QVariant {
+ const auto key = sourceRoleNames.key(rolename, -1);
+ if (key == -1)
+ return {};
+ return idx.data(key);
+ };
+
+ const auto srcIndex = m_sourceModel->index(index, 0);
+ const auto symbol = dataForIndex(srcIndex, kSymbolRoleName).toString();
+ const auto communityId = dataForIndex(srcIndex, kCommunityIdRoleName).toString();
+ const auto communityName = dataForIndex(srcIndex, kCommunityNameRoleName).toString();
+ const auto visible = m_settingsData.contains(symbol) ? std::get<1>(m_settingsData.value(symbol)) : true;
+
+ TokenData token;
+ token.symbol = symbol;
+ token.name = dataForIndex(srcIndex, kNameRoleName).toString();
+ token.image = dataForIndex(srcIndex, kTokenImageRoleName).toString();
+ token.communityId = communityId;
+ token.communityName = !communityName.isEmpty() ? communityName : communityId;
+ token.communityImage = dataForIndex(srcIndex, kCommunityImageRoleName).toString();
+ token.collectionUid = dataForIndex(srcIndex, kCollectionUidRoleName).toString();
+ token.collectionName = dataForIndex(srcIndex, kCollectionNameRoleName).toString();
+ token.balance = dataForIndex(srcIndex, kEnabledNetworkBalanceRoleName);
+ token.currencyBalance = dataForIndex(srcIndex, kEnabledNetworkCurrencyBalanceRoleName);
+
+ token.customSortOrderNo = m_settingsData.contains(symbol) ? std::get<0>(m_settingsData.value(symbol))
+ : (visible ? INT_MAX : 0); // append/prepend
+
+ if (!visible)
+ m_hiddenTokensModel->addItem(token, /*append*/ false);
+ else if (!communityId.isEmpty())
+ m_communityTokensModel->addItem(token);
+ else
+ m_regularTokensModel->addItem(token);
+}
+
+bool ManageTokensController::dirty() const
+{
+ return std::any_of(m_allModels.cbegin(), m_allModels.cend(), [](auto model) {
+ return model->dirty();
+ });
+}
+
+bool ManageTokensController::arrangeByCommunity() const
+{
+ return m_arrangeByCommunity;
+}
+
+void ManageTokensController::setArrangeByCommunity(bool newArrangeByCommunity)
+{
+ if(m_arrangeByCommunity == newArrangeByCommunity) return;
+ m_arrangeByCommunity = newArrangeByCommunity;
+ if (!m_arrangeByCommunity)
+ m_communityTokensModel->applySort();
+ else
+ rebuildCommunityTokenGroupsModel();
+ emit arrangeByCommunityChanged();
+}
+
+void ManageTokensController::reloadCommunityIds()
+{
+ m_communityIds.clear();
+ auto model = m_arrangeByCommunity ? m_communityTokenGroupsModel : m_communityTokensModel;
+ const auto count = model->count();
+ for (int i = 0; i < count; i++) {
+ const auto& token = model->itemAt(i);
+ if (!m_communityIds.contains(token.communityId))
+ m_communityIds.append(token.communityId);
+ }
+ qCDebug(manageTokens) << "!!! FOUND UNIQUE COMMUNITY GROUP IDs:" << m_communityIds;
+}
+
+void ManageTokensController::rebuildCommunityTokenGroupsModel()
+{
+ QStringList communityIds;
+ QList result;
+
+ const auto count = m_communityTokensModel->count();
+ for (auto i = 0; i < count; i++) {
+ const auto& communityToken = m_communityTokensModel->itemAt(i);
+ const auto communityId = communityToken.communityId;
+ if (!communityIds.contains(communityId)) { // insert into groups
+ communityIds.append(communityId);
+
+ TokenData tokenGroup;
+ tokenGroup.communityId = communityId;
+ tokenGroup.communityName = communityToken.communityName;
+ tokenGroup.communityImage = communityToken.communityImage;
+ tokenGroup.balance = 1;
+ result.append(tokenGroup);
+ } else { // update group's childCount
+ const auto tokenGroup = std::find_if(result.cbegin(), result.cend(), [communityId](const auto& item) {
+ return communityId == item.communityId;
+ });
+ if (tokenGroup != result.cend()) {
+ const auto row = std::distance(result.cbegin(), tokenGroup);
+ TokenData updTokenGroup = result.takeAt(row);
+ updTokenGroup.balance = updTokenGroup.balance.toInt() + 1;
+ result.insert(row, updTokenGroup);
+ }
+ }
+ }
+
+ m_communityTokenGroupsModel->clear();
+ for (const auto& group: result)
+ m_communityTokenGroupsModel->addItem(group);
+
+ qCDebug(manageTokens) << "!!! GROUPS MODEL REBUILT WITH GROUPS:" << communityIds;
+}
+
+QString ManageTokensController::settingsKey() const
+{
+ return m_settingsKey;
+}
+
+void ManageTokensController::setSettingsKey(const QString& newSettingsKey)
+{
+ if (m_settingsKey == newSettingsKey)
+ return;
+ m_settingsKey = newSettingsKey;
+ emit settingsKeyChanged();
+}
diff --git a/ui/StatusQ/src/wallet/managetokenscontroller.h b/ui/StatusQ/src/wallet/managetokenscontroller.h
new file mode 100644
index 00000000000..908baf2520b
--- /dev/null
+++ b/ui/StatusQ/src/wallet/managetokenscontroller.h
@@ -0,0 +1,94 @@
+#include
+#include
+#include
+
+#include
+
+#include "managetokensmodel.h"
+
+class QAbstractItemModel;
+
+class ManageTokensController : public QObject, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+
+ // input properties
+ Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged FINAL REQUIRED)
+ Q_PROPERTY(QString settingsKey READ settingsKey WRITE setSettingsKey NOTIFY settingsKeyChanged FINAL REQUIRED)
+ Q_PROPERTY(bool arrangeByCommunity READ arrangeByCommunity WRITE setArrangeByCommunity NOTIFY arrangeByCommunityChanged FINAL)
+
+ // output properties
+ Q_PROPERTY(QAbstractItemModel* regularTokensModel READ regularTokensModel CONSTANT FINAL)
+ // TODO regularTokenGroupsModel for grouped (collections of) collectibles?
+ Q_PROPERTY(QAbstractItemModel* communityTokensModel READ communityTokensModel CONSTANT FINAL)
+ Q_PROPERTY(QAbstractItemModel* communityTokenGroupsModel READ communityTokenGroupsModel CONSTANT FINAL)
+ Q_PROPERTY(QAbstractItemModel* hiddenTokensModel READ hiddenTokensModel CONSTANT FINAL)
+ Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
+
+public:
+ explicit ManageTokensController(QObject* parent = nullptr);
+
+ Q_INVOKABLE void showHideRegularToken(int row, bool flag);
+ Q_INVOKABLE void showHideCommunityToken(int row, bool flag);
+ Q_INVOKABLE void showHideGroup(const QString& groupId, bool flag);
+
+ Q_INVOKABLE void saveSettings();
+ Q_INVOKABLE void clearSettings();
+ Q_INVOKABLE void revert();
+
+ // TODO: to be used by SFPM on the main wallet page as an "expressionRole"
+ // bool lessThan(lhsSymbol, rhsSymbol) const;
+ // bool filterAcceptsRow(index or symbol?) const;
+
+protected:
+ void classBegin() override;
+ void componentComplete() override;
+
+signals:
+ void sourceModelChanged();
+ void dirtyChanged();
+ void arrangeByCommunityChanged();
+ void settingsKeyChanged();
+
+private:
+ QAbstractItemModel* m_sourceModel{nullptr};
+ QAbstractItemModel* sourceModel() const { return m_sourceModel; }
+ void setSourceModel(QAbstractItemModel* newSourceModel);
+ void parseSourceModel();
+
+ void addItem(int index);
+
+ ManageTokensModel* m_regularTokensModel{nullptr};
+ QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; };
+
+ ManageTokensModel* m_communityTokensModel{nullptr};
+ QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; };
+
+ ManageTokensModel* m_communityTokenGroupsModel{nullptr};
+ QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; };
+
+ ManageTokensModel* m_hiddenTokensModel{nullptr};
+ QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; };
+
+ bool dirty() const;
+
+ bool m_arrangeByCommunity{false};
+ bool arrangeByCommunity() const;
+ void setArrangeByCommunity(bool newArrangeByCommunity);
+
+ QStringList m_communityIds;
+ void reloadCommunityIds();
+ void rebuildCommunityTokenGroupsModel();
+
+ const std::array m_allModels {m_regularTokensModel, m_communityTokensModel, m_communityTokenGroupsModel, m_hiddenTokensModel};
+
+ QString m_settingsKey;
+ QString settingsKey() const;
+ void setSettingsKey(const QString& newSettingsKey);
+ QSettings m_settings;
+ void loadSettings();
+ SerializedTokenData m_settingsData; // symbol -> {sortOrder, visible, groupId}
+
+ bool m_modelConnectionsInitialized{false};
+};
diff --git a/ui/StatusQ/src/wallet/managetokensmodel.cpp b/ui/StatusQ/src/wallet/managetokensmodel.cpp
new file mode 100644
index 00000000000..ea19f1f0ffa
--- /dev/null
+++ b/ui/StatusQ/src/wallet/managetokensmodel.cpp
@@ -0,0 +1,195 @@
+#include "managetokensmodel.h"
+
+#include
+
+Q_LOGGING_CATEGORY(manageTokens, "status.models.manageTokens", QtInfoMsg)
+
+ManageTokensModel::ManageTokensModel(QObject* parent)
+ : QAbstractListModel(parent)
+{
+ connect(this, &QAbstractItemModel::rowsInserted, this, &ManageTokensModel::countChanged);
+ connect(this, &QAbstractItemModel::rowsRemoved, this, &ManageTokensModel::countChanged);
+ connect(this, &QAbstractItemModel::modelReset, this, &ManageTokensModel::countChanged);
+ connect(this, &QAbstractItemModel::layoutChanged, this, &ManageTokensModel::countChanged);
+}
+
+void ManageTokensModel::moveItem(int fromRow, int toRow)
+{
+ qCDebug(manageTokens) << Q_FUNC_INFO << "from" << fromRow << "to" << toRow;
+
+ if (toRow < 0 || toRow >= rowCount() || fromRow < 0 || fromRow >= rowCount())
+ return;
+
+ auto destRow = toRow;
+ if (toRow > fromRow)
+ destRow++;
+
+ beginMoveRows({}, fromRow, fromRow, {}, destRow);
+ m_data.move(fromRow, toRow);
+ endMoveRows();
+ setDirty(true);
+}
+
+void ManageTokensModel::addItem(const TokenData& item, bool append)
+{
+ const auto destRow = append ? rowCount() : 0;
+ beginInsertRows({}, destRow, destRow);
+ append ? m_data.append(item) : m_data.prepend(item);
+ endInsertRows();
+ setDirty(true);
+}
+
+std::optional ManageTokensModel::takeItem(int row)
+{
+ if (row < 0 || row >= rowCount())
+ return {};
+
+ beginRemoveRows({}, row, row);
+ auto res = m_data.takeAt(row);
+ endRemoveRows();
+ setDirty(true);
+ return res;
+}
+
+QList ManageTokensModel::takeAllItems(const QString& communityId)
+{
+ QList result;
+ QList indexesToRemove;
+
+ for (int i = 0; i < m_data.count(); i++) {
+ const auto &token = m_data.at(i);
+ if (token.communityId == communityId) {
+ result.append(token);
+ indexesToRemove.append(i);
+ }
+ }
+
+ QList::reverse_iterator its;
+ for(its = indexesToRemove.rbegin(); its != indexesToRemove.rend(); ++its) {
+ const auto row = *its;
+ beginRemoveRows({}, row, row);
+ m_data.removeAt(row);
+ endRemoveRows();
+ }
+
+ setDirty(true);
+ return result;
+}
+
+void ManageTokensModel::clear()
+{
+ beginResetModel();
+ m_data.clear();
+ endResetModel();
+ setDirty(false);
+}
+
+SerializedTokenData ManageTokensModel::save(bool isVisible)
+{
+ saveCustomSortOrder();
+ const auto size = count();
+ SerializedTokenData result;
+ for (int i = 0; i < size; i++) {
+ const auto& token = itemAt(i);
+ const auto groupId = !token.communityId.isEmpty() ? token.communityId : token.collectionUid;
+ result.insert(token.symbol, {i, isVisible, groupId});
+ }
+ setDirty(false);
+ return result;
+}
+
+int ManageTokensModel::rowCount(const QModelIndex& parent) const
+{
+ return m_data.size();
+}
+
+QHash ManageTokensModel::roleNames() const
+{
+ static const QHash roles {
+ {SymbolRole, kSymbolRoleName},
+ {NameRole, kNameRoleName},
+ {CommunityIdRole, kCommunityIdRoleName},
+ {CommunityNameRole, kCommunityNameRoleName},
+ {CommunityImageRole, kCommunityImageRoleName},
+ {CollectionUidRole, kCollectionUidRoleName},
+ {CollectionNameRole, kCollectionNameRoleName},
+ {BalanceRole, kEnabledNetworkBalanceRoleName},
+ {CurrencyBalanceRole, kEnabledNetworkCurrencyBalanceRoleName},
+ {CustomSortOrderNoRole, kCustomSortOrderNoRoleName},
+ {TokenImageRole, kTokenImageRoleName},
+ };
+
+ return roles;
+}
+
+QVariant ManageTokensModel::data(const QModelIndex& index, int role) const
+{
+ if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid))
+ return {};
+
+ const auto& token = m_data.at(index.row());
+
+ switch(static_cast(role))
+ {
+ case SymbolRole: return token.symbol;
+ case NameRole: return token.name;
+ case CommunityIdRole: return token.communityId;
+ case CommunityNameRole: return token.communityName;
+ case CommunityImageRole: return token.communityImage;
+ case CollectionUidRole: return token.collectionUid;
+ case CollectionNameRole: return token.collectionName;
+ case BalanceRole: return token.balance;
+ case CurrencyBalanceRole: return token.currencyBalance;
+ case CustomSortOrderNoRole: return token.customSortOrderNo;
+ case TokenImageRole: return token.image;
+ }
+
+ return {};
+}
+
+bool ManageTokensModel::dirty() const
+{
+ return m_dirty;
+}
+
+void ManageTokensModel::setDirty(bool flag)
+{
+ if (m_dirty == flag) return;
+ m_dirty = flag;
+ emit dirtyChanged();
+}
+
+void ManageTokensModel::saveCustomSortOrder()
+{
+ const auto count = rowCount();
+ for (auto i = 0; i < count; i++) {
+ TokenData newToken{m_data.at(i)};
+ if (newToken.communityId.isEmpty()) {
+ newToken.customSortOrderNo = i;
+ } else {
+ const auto communityIdx = m_communityIds.indexOf(newToken.communityId) + 1;
+ newToken.customSortOrderNo = i + (communityIdx * 100'000);
+ }
+ m_data[i] = newToken;
+ }
+ emit dataChanged(index(0, 0), index(count - 1, 0), {TokenDataRoles::CustomSortOrderNoRole});
+}
+
+void ManageTokensModel::applySort()
+{
+ emit layoutAboutToBeChanged({}, QAbstractItemModel::VerticalSortHint);
+
+ // clazy:exclude=clazy-detaching-member
+ std::stable_sort(m_data.begin(), m_data.end(), [this](const TokenData& lhs, const TokenData& rhs) {
+ return lhs.customSortOrderNo < rhs.customSortOrderNo;
+ });
+
+ emit layoutChanged({}, QAbstractItemModel::VerticalSortHint);
+}
+
+bool ManageTokensModel::hasCommunityIdToken(const QString& communityId) const
+{
+ return std::any_of(m_data.cbegin(), m_data.constEnd(), [communityId](const auto& token) {
+ return token.communityId == communityId;
+ });
+}
diff --git a/ui/StatusQ/src/wallet/managetokensmodel.h b/ui/StatusQ/src/wallet/managetokensmodel.h
new file mode 100644
index 00000000000..53a1350af08
--- /dev/null
+++ b/ui/StatusQ/src/wallet/managetokensmodel.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(manageTokens)
+
+namespace
+{
+const auto kSymbolRoleName = QByteArrayLiteral("symbol");
+const auto kNameRoleName = QByteArrayLiteral("name");
+const auto kCommunityIdRoleName = QByteArrayLiteral("communityId");
+const auto kCommunityNameRoleName = QByteArrayLiteral("communityName");
+const auto kCommunityImageRoleName = QByteArrayLiteral("communityImage");
+const auto kCollectionUidRoleName = QByteArrayLiteral("collectionUid");
+const auto kCollectionNameRoleName = QByteArrayLiteral("collectionName");
+const auto kEnabledNetworkBalanceRoleName = QByteArrayLiteral("enabledNetworkBalance");
+const auto kEnabledNetworkCurrencyBalanceRoleName = QByteArrayLiteral("enabledNetworkCurrencyBalance");
+const auto kCustomSortOrderNoRoleName = QByteArrayLiteral("customSortOrderNo");
+const auto kTokenImageRoleName = QByteArrayLiteral("imageUrl");
+} // namespace
+
+struct TokenData {
+ QString symbol, name, communityId, communityName, communityImage, collectionUid, collectionName, image;
+ QVariant balance, currencyBalance;
+ int customSortOrderNo{-1};
+};
+
+// symbol -> {sortOrder, visible, groupId}
+using SerializedTokenData = QHash>;
+
+class ManageTokensModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int count READ count NOTIFY countChanged FINAL)
+ Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
+
+public:
+ enum TokenDataRoles {
+ SymbolRole = Qt::UserRole + 1,
+ NameRole,
+ CommunityIdRole,
+ CommunityNameRole,
+ CommunityImageRole,
+ CollectionUidRole,
+ CollectionNameRole,
+ BalanceRole,
+ CurrencyBalanceRole,
+ CustomSortOrderNoRole,
+ TokenImageRole,
+ };
+ Q_ENUM(TokenDataRoles)
+
+ explicit ManageTokensModel(QObject* parent = nullptr);
+
+ Q_INVOKABLE void moveItem(int fromRow, int toRow);
+
+ void addItem(const TokenData& item, bool append = true);
+ std::optional takeItem(int row);
+ QList takeAllItems(const QString& communityId);
+ void clear();
+
+ SerializedTokenData save(bool isVisible = true);
+
+ bool dirty() const;
+ void setDirty(bool flag);
+
+ void saveCustomSortOrder();
+ void applySort();
+
+ int count() const { return rowCount(); }
+ const TokenData& itemAt(int row) const { return m_data.at(row); }
+
+ void setCommunityIds(const QStringList& ids) { m_communityIds = ids; };
+ bool hasCommunityIdToken(const QString& communityId) const;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QHash roleNames() const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+signals:
+ void countChanged();
+ void dirtyChanged();
+
+private:
+ QStringList m_communityIds;
+
+ bool m_dirty{false};
+
+ QList m_data;
+};
diff --git a/ui/app/AppLayouts/Profile/controls/CollectibleShowcaseDelegate.qml b/ui/app/AppLayouts/Profile/controls/CollectibleShowcaseDelegate.qml
index 15cff77a389..2c4a7227bf5 100644
--- a/ui/app/AppLayouts/Profile/controls/CollectibleShowcaseDelegate.qml
+++ b/ui/app/AppLayouts/Profile/controls/CollectibleShowcaseDelegate.qml
@@ -9,5 +9,5 @@ ShowcaseDelegate {
icon.source: hasImage ? showcaseObj.imageUrl : ""
bgRadius: Style.current.radius
- bgColor: !!showcaseObj && !!showcaseObj.backgroundColor ? showcaseObj.backgroundColor : "transparent"
+ assetBgColor: !!showcaseObj && !!showcaseObj.backgroundColor ? showcaseObj.backgroundColor : "transparent"
}
diff --git a/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml b/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml
index eb0c4f5c509..79003c3f2da 100644
--- a/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml
+++ b/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml
@@ -90,7 +90,6 @@ StatusDraggableListItem {
}
}
}
-
}
]
}
diff --git a/ui/app/AppLayouts/Wallet/controls/ManageTokenMenuButton.qml b/ui/app/AppLayouts/Wallet/controls/ManageTokenMenuButton.qml
new file mode 100644
index 00000000000..9c8b1f49195
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/controls/ManageTokenMenuButton.qml
@@ -0,0 +1,127 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+import StatusQ.Core 0.1
+import StatusQ.Components 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Popups 0.1
+
+StatusFlatButton {
+ id: root
+
+ property int currentIndex
+ property int count
+
+ property bool inHidden
+ property bool isGroup
+ property string groupId
+ property bool isCommunityAsset
+
+ readonly property bool hideEnabled: model.symbol !== "ETH"
+ readonly property bool menuVisible: menuLoader.active
+
+ signal moveRequested(int from, int to)
+ signal showHideRequested(int index, bool flag)
+ signal showHideGroupRequested(string groupId, bool flag)
+
+ icon.name: "more"
+ horizontalPadding: 4
+ verticalPadding: 4
+ textColor: hovered || highlighted ? Theme.palette.directColor1 : Theme.palette.baseColor1
+ highlighted: menuLoader.item && menuLoader.item.opened
+
+ onClicked: {
+ menuLoader.active = true
+ menuLoader.item.popup(width - menuLoader.item.width, height)
+ }
+
+ Loader {
+ id: menuLoader
+ active: false
+ sourceComponent: StatusMenu {
+ onClosed: menuLoader.active = false
+
+ StatusAction {
+ enabled: !root.inHidden && root.currentIndex !== 0
+ icon.name: "arrow-top"
+ text: qsTr("Move to top")
+ onTriggered: root.moveRequested(root.currentIndex, 0)
+ }
+ StatusAction {
+ enabled: !root.inHidden && root.currentIndex !== 0
+ icon.name: "arrow-up"
+ text: qsTr("Move up")
+ onTriggered: root.moveRequested(root.currentIndex, root.currentIndex - 1)
+ }
+ StatusAction {
+ enabled: !root.inHidden && root.currentIndex < root.count - 1
+ icon.name: "arrow-down"
+ text: qsTr("Move down")
+ onTriggered: root.moveRequested(root.currentIndex, root.currentIndex + 1)
+ }
+ StatusAction {
+ enabled: !root.inHidden && root.currentIndex < root.count - 1
+ icon.name: "arrow-bottom"
+ text: qsTr("Move to bottom")
+ onTriggered: root.moveRequested(root.currentIndex, root.count - 1)
+ }
+
+ StatusMenuSeparator { enabled: !root.inHidden && root.hideEnabled }
+
+ // any token
+ StatusAction {
+ enabled: !root.inHidden && root.hideEnabled && !root.isGroup && !root.isCommunityAsset
+ type: StatusAction.Type.Danger
+ icon.name: "hide"
+ text: qsTr("Hide asset")
+ onTriggered: root.showHideRequested(root.currentIndex, false)
+ }
+ StatusAction {
+ enabled: root.inHidden
+ icon.name: "show"
+ text: qsTr("Show asset")
+ onTriggered: root.showHideRequested(root.currentIndex, true)
+ }
+
+ // (hide) community tokens
+ StatusMenu {
+ id: communitySubmenu
+ enabled: !root.inHidden && root.isCommunityAsset
+ title: qsTr("Hide")
+ assetSettings.name: "hide"
+ type: StatusAction.Type.Danger
+
+ StatusAction {
+ text: qsTr("This asset")
+ onTriggered: {
+ root.showHideRequested(root.currentIndex, false)
+ communitySubmenu.dismiss()
+ }
+ }
+ StatusAction {
+ text: qsTr("All assets from this community")
+ onTriggered: {
+ root.showHideGroupRequested(root.groupId, false)
+ communitySubmenu.dismiss()
+ }
+ }
+ }
+
+ // token group
+ StatusAction {
+ enabled: !root.inHidden && root.isGroup
+ type: StatusAction.Type.Danger
+ icon.name: "hide"
+ text: qsTr("Hide all assets from this community")
+ onTriggered: root.showHideGroupRequested(root.groupId, false)
+ }
+ StatusAction {
+ enabled: root.inHidden && root.groupId
+ icon.name: "show"
+ text: qsTr("Show all assets from this community")
+ onTriggered: root.showHideGroupRequested(root.groupId, true)
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml
new file mode 100644
index 00000000000..9551d99f4b2
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml
@@ -0,0 +1,272 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.15
+
+import StatusQ.Core 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+
+ComboBox {
+ id: root
+
+ property int sortOrder: Qt.DescendingOrder
+ readonly property string currentSortRoleName: d.currentSortRoleName
+
+ model: d.predefinedSortModel
+ textRole: "text"
+ valueRole: "value"
+ displayText: !d.isCustomSortOrder ? "%1 %2".arg(currentText).arg(sortOrder === Qt.DescendingOrder ? "↓" : "↑")
+ : currentText
+
+ Component.onCompleted: currentIndex = indexOfValue(SortOrderComboBox.TokenOrderCustom)
+
+ enum TokenOrder {
+ TokenOrderNone = 0,
+ TokenOrderCustom,
+ TokenOrderValue,
+ TokenOrderBalance,
+ TokenOrder1WChange,
+ TokenOrderAlpha
+ }
+
+ horizontalPadding: 12
+ verticalPadding: 8
+ spacing: 8
+
+ font.family: Theme.palette.baseFont.name
+ font.pixelSize: Style.current.additionalTextSize
+
+
+ QtObject {
+ id: d
+
+ readonly property int defaultDelegateHeight: 34
+
+// // models
+// readonly property SortFilterProxyModel tokensModel: SortFilterProxyModel {
+// sourceModel: root.baseModel
+// proxyRoles: [
+// ExpressionRole {
+// name: "currentBalance"
+// expression: model.enabledNetworkBalance.amount
+// },
+// ExpressionRole {
+// name: "currentCurrencyBalance"
+// expression: model.enabledNetworkCurrencyBalance.amount
+// }
+// ]
+// sorters: RoleSorter {
+// roleName: cmbTokenOrder.currentSortRoleName
+// sortOrder: cmbTokenOrder.sortOrder
+// enabled: !d.isCustomSortOrder
+// }
+// filters: ValueFilter {
+// roleName: "visibleForNetworkWithPositiveBalance"
+// value: true
+// }
+// }
+
+ readonly property var predefinedSortModel: [
+ { value: SortOrderComboBox.TokenOrderValue, text: qsTr("Token value"), icon: "token-sale", sortRoleName: "currentCurrencyBalance" }, // custom SFPM ExpressionRole
+ { value: SortOrderComboBox.TokenOrderBalance, text: qsTr("Token balance"), icon: "wallet", sortRoleName: "currentBalance" }, // custom SFPM ExpressionRole
+ { value: SortOrderComboBox.TokenOrder1WChange, text: qsTr("1W change"), icon: "time", sortRoleName: "changePct24hour" }, // FIXME changePct1Week role missing in backend!!!
+ { value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Alphabetic"), icon: "bold", sortRoleName: "name" },
+ { value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" },
+ { value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" }
+ ]
+ readonly property string currentSortRoleName: root.currentIndex !== -1 ? d.predefinedSortModel[root.currentIndex].sortRoleName : ""
+ readonly property bool isCustomSortOrder: root.currentValue === SortOrderComboBox.TokenOrderCustom
+ }
+
+ background: Rectangle {
+ border.width: 1
+ border.color: Theme.palette.directColor7
+ radius: 8
+ color: root.down ? Theme.palette.baseColor2 : "transparent"
+ HoverHandler {
+ cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
+ }
+ }
+
+ contentItem: StatusBaseText {
+ leftPadding: root.horizontalPadding
+ rightPadding: root.horizontalPadding
+ font.pixelSize: root.font.pixelSize
+ font.weight: Font.Medium
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: root.displayText
+ color: Theme.palette.baseColor1
+ }
+
+ indicator: StatusIcon {
+ x: root.mirrored ? root.horizontalPadding : root.width - width - root.horizontalPadding
+ y: root.topPadding + (root.availableHeight - height) / 2
+ width: 16
+ height: width
+ icon: "chevron-down"
+ color: Theme.palette.baseColor1
+ }
+
+ popup: Popup {
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+ y: root.height + 4
+
+ implicitWidth: root.width
+ margins: 8
+
+ padding: 1
+ verticalPadding: 8
+
+ background: Rectangle {
+ color: Theme.palette.statusSelect.menuItemBackgroundColor
+ radius: 8
+ border.color: Theme.palette.baseColor2
+ layer.enabled: true
+ layer.effect: DropShadow {
+ horizontalOffset: 0
+ verticalOffset: 4
+ radius: 12
+ samples: 25
+ spread: 0.2
+ color: Theme.palette.dropShadow
+ }
+ }
+
+ contentItem: ColumnLayout {
+ StatusBaseText {
+ Layout.fillWidth: true
+ Layout.preferredHeight: d.defaultDelegateHeight
+ text: qsTr("Sort by")
+ font.pixelSize: Style.current.tertiaryTextFontSize
+ leftPadding: Style.current.padding
+ verticalAlignment: Qt.AlignVCenter
+ color: Theme.palette.baseColor1
+ }
+ StatusListView {
+ Layout.fillWidth: true
+ implicitWidth: contentWidth
+ implicitHeight: contentHeight
+
+ model: root.popup.visible ? root.delegateModel : null
+ currentIndex: root.highlightedIndex
+ }
+ }
+ }
+
+ Component {
+ id: regularMenuComponent
+ RowLayout {
+ spacing: root.spacing
+
+ StatusIcon {
+ visible: !!icon
+ icon: iconName
+ color: root.enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
+ width: 16
+ height: 16
+ }
+
+ StatusBaseText {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ text: menuText
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ color: root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1
+ font.pixelSize: root.font.pixelSize
+ font.weight: root.currentIndex === menuIndex ? Font.DemiBold : Font.Normal
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Row {
+ visible: !isCustomOrder
+ spacing: 4
+ StatusFlatRoundButton {
+ radius: 6
+ width: 24
+ height: 24
+ icon.name: "arrow-up"
+ icon.width: 18
+ icon.height: 18
+ opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put
+ highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.AscendingOrder
+ onClicked: {
+ if (root.currentIndex !== menuIndex)
+ root.currentIndex = menuIndex
+ root.sortOrder = Qt.AscendingOrder
+ root.popup.close()
+ }
+ }
+ StatusFlatRoundButton {
+ radius: 6
+ width: 24
+ height: 24
+ icon.name: "arrow-down"
+ icon.width: 18
+ icon.height: 18
+ opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put
+ highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.DescendingOrder
+ onClicked: {
+ if (root.currentIndex !== menuIndex)
+ root.currentIndex = menuIndex
+ root.sortOrder = Qt.DescendingOrder
+ root.popup.close()
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: separatorMenuComponent
+ StatusMenuSeparator {}
+ }
+
+ delegate: ItemDelegate {
+ required property int index
+ required property var modelData
+ readonly property bool isSeparator: text === "---"
+
+ id: menuDelegate
+ width: root.width
+ highlighted: root.highlightedIndex === index
+ enabled: !isSeparator
+ leftPadding: isSeparator ? 0 : 14
+ rightPadding: isSeparator ? 0 : 8
+ verticalPadding: isSeparator ? 2 : 5
+ spacing: root.spacing
+ font: root.font
+ text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole])
+ : modelData
+ icon.name: modelData["icon"]
+ icon.color: Theme.palette.primaryColor1
+ background: Rectangle {
+ implicitHeight: parent.isSeparator ? 3 : d.defaultDelegateHeight
+ color: {
+ if (menuDelegate.index === root.currentIndex)
+ return Theme.palette.primaryColor3
+ if (menuDelegate.highlighted)
+ return Theme.palette.statusMenu.hoverBackgroundColor
+
+ return "transparent"
+ }
+ HoverHandler {
+ cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
+ }
+ }
+ contentItem: Loader {
+ readonly property int menuIndex: menuDelegate.index
+ readonly property string menuText: menuDelegate.text
+ readonly property string iconName: menuDelegate.icon.name
+ readonly property bool isCustomOrder: !menuDelegate.modelData["sortRoleName"]
+ sourceComponent: menuDelegate.isSeparator ? separatorMenuComponent : regularMenuComponent
+ }
+ onClicked: root.currentIndex = index
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/controls/qmldir b/ui/app/AppLayouts/Wallet/controls/qmldir
index bc56afce9d4..fdcd6f1c49d 100644
--- a/ui/app/AppLayouts/Wallet/controls/qmldir
+++ b/ui/app/AppLayouts/Wallet/controls/qmldir
@@ -4,3 +4,5 @@ AccountHeaderGradient 1.0 AccountHeaderGradient.qml
StatusTxProgressBar 1.0 StatusTxProgressBar.qml
StatusDateRangePicker 1.0 StatusDateRangePicker.qml
ActivityFilterTagItem 1.0 ActivityFilterTagItem.qml
+SortOrderComboBox 1.0 SortOrderComboBox.qml
+ManageTokenMenuButton 1.0 ManageTokenMenuButton.qml
diff --git a/ui/app/AppLayouts/Wallet/panels/ManageTokensPanel.qml b/ui/app/AppLayouts/Wallet/panels/ManageTokensPanel.qml
new file mode 100644
index 00000000000..6557a353dec
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/panels/ManageTokensPanel.qml
@@ -0,0 +1,378 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ.Core 0.1
+import StatusQ.Components 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Popups 0.1
+import StatusQ.Models 0.1
+
+import utils 1.0
+import shared.controls 1.0
+
+import AppLayouts.Wallet.controls 1.0
+
+Control {
+ id: root
+
+ required property var baseModel
+
+ readonly property bool dirty: d.controller.dirty
+
+ background: null
+
+ function saveSettings() {
+ d.controller.saveSettings();
+ }
+
+ function revert() {
+ d.controller.revert();
+ }
+
+ function clearSettings() {
+ d.controller.clearSettings();
+ }
+
+ QtObject {
+ id: d
+
+ property bool communityGroupsExpanded: true
+
+ readonly property var controller: ManageTokensController {
+ sourceModel: root.baseModel
+ arrangeByCommunity: switchArrangeByCommunity.checked
+ settingsKey: "WalletAssets"
+ }
+ }
+
+ component CommunityTag: InformationTag {
+ tagPrimaryLabel.font.weight: Font.Medium
+ customBackground: Component {
+ Rectangle {
+ color: Theme.palette.baseColor4
+ radius: 20
+ }
+ }
+ }
+
+ component LocalTokenDelegate: DropArea {
+ id: delegateRoot
+
+ property int visualIndex: index
+ property alias dragEnabled: delegate.dragEnabled
+ property alias bgColor: delegate.bgColor
+ property alias topInset: delegate.topInset
+ property alias bottomInset: delegate.bottomInset
+ property bool isGrouped
+ property bool isHidden
+ property int count
+
+ ListView.onRemove: SequentialAnimation {
+ PropertyAction { target: delegateRoot; property: "ListView.delayRemove"; value: true }
+ NumberAnimation { target: delegateRoot; property: "scale"; to: 0; easing.type: Easing.InOutQuad }
+ PropertyAction { target: delegateRoot; property: "ListView.delayRemove"; value: false }
+ }
+
+ width: ListView.view.width
+ height: visible ? delegate.height : 0
+
+ onEntered: function(drag) {
+ var from = drag.source.visualIndex
+ var to = delegate.visualIndex
+ if (to === from)
+ return
+ //console.warn("!!! DROP from/to", from, to)
+ ListView.view.model.moveItem(from, to)
+ drag.accept()
+ }
+
+ StatusDraggableListItem {
+ id: delegate
+
+ visualIndex: index
+ dragParent: root
+ Drag.keys: delegateRoot.keys
+ draggable: true
+
+ width: delegateRoot.width
+ title: model.name// + " (%1 -> %2)".arg(index).arg(model.customSortOrderNo)
+ secondaryTitle: hovered || menuBtn.menuVisible ? "%1 · %2".arg(LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkBalance))
+ .arg(LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkCurrencyBalance))
+ : LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkBalance)
+ hasImage: true
+ icon.source: model.imageUrl || Constants.tokenIcon(model.symbol)
+ icon.width: 32
+ icon.height: 32
+ spacing: 12
+
+ actions: [
+ CommunityTag {
+ tagPrimaryLabel.text: model.communityName
+ visible: !!model.communityId && !delegateRoot.isGrouped
+ image.source: model.communityImage
+ },
+ ManageTokenMenuButton {
+ id: menuBtn
+ currentIndex: visualIndex
+ count: delegateRoot.count
+ inHidden: delegateRoot.isHidden
+ groupId: model.communityId
+ isCommunityAsset: !!model.communityId
+ onMoveRequested: (from, to) => isCommunityAsset ? d.controller.communityTokensModel.moveItem(from, to)
+ : d.controller.regularTokensModel.moveItem(from, to)
+ onShowHideRequested: (index, flag) => isCommunityAsset ? d.controller.showHideCommunityToken(index, flag)
+ : d.controller.showHideRegularToken(index, flag)
+ onShowHideGroupRequested: (groupId, flag) => d.controller.showHideGroup(groupId, flag)
+ }
+ ]
+ }
+ }
+
+ component LocalTokenGroupDelegate: DropArea {
+ id: communityDelegateRoot
+
+ property int visualIndex: index
+ readonly property string communityId: model.communityId
+ readonly property int childCount: model.enabledNetworkBalance // NB using "balance" as "count" in m_communityTokenGroupsModel
+
+ ListView.onRemove: SequentialAnimation {
+ PropertyAction { target: communityDelegateRoot; property: "ListView.delayRemove"; value: true }
+ NumberAnimation { target: communityDelegateRoot; property: "scale"; to: 0; easing.type: Easing.InOutQuad }
+ PropertyAction { target: communityDelegateRoot; property: "ListView.delayRemove"; value: false }
+ }
+
+ keys: ["x-status-draggable-community-group-item"]
+ visible: childCount
+ width: ListView.view.width
+ height: visible ? groupedCommunityTokenDelegate.implicitHeight : 0
+
+ onEntered: function(drag) {
+ var from = drag.source.visualIndex
+ var to = groupedCommunityTokenDelegate.visualIndex
+ if (to === from)
+ return
+ //console.warn("!!! DROP GROUP from/to", from, to)
+ ListView.view.model.moveItem(from, to)
+ drag.accept()
+ }
+
+ StatusDraggableListItem {
+ id: groupedCommunityTokenDelegate
+ width: parent.width
+ height: dragActive ? implicitHeight : parent.height
+ leftPadding: Style.current.halfPadding
+ rightPadding: Style.current.halfPadding
+ bottomPadding: Style.current.halfPadding
+ topPadding: 22
+ draggable: true
+ spacing: 12
+ bgColor: Theme.palette.baseColor4
+
+ visualIndex: index
+ dragParent: root
+ Drag.keys: communityDelegateRoot.keys
+
+ contentItem: ColumnLayout {
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.leftMargin: 12
+ Layout.rightMargin: 12
+ Layout.bottomMargin: 14
+ spacing: groupedCommunityTokenDelegate.spacing
+
+ StatusIcon {
+ Layout.preferredWidth: 20
+ Layout.preferredHeight: 20
+ icon: "justify"
+ color: Theme.palette.baseColor1
+ }
+
+ StatusRoundedImage {
+ radius: groupedCommunityTokenDelegate.bgRadius
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ image.source: model.communityImage
+ showLoadingIndicator: true
+ image.fillMode: Image.PreserveAspectCrop
+ }
+
+ StatusBaseText {
+ text: model.communityName// + "(%1 -> %2)".arg(index).arg(model.customSortOrderNo)
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ font.weight: Font.Medium
+ }
+
+ StatusBaseText {
+ Layout.leftMargin: -parent.spacing/2
+ text: "· %1".arg(qsTr("%n asset(s)", "", communityDelegateRoot.childCount))
+ elide: Text.ElideRight
+ color: Theme.palette.baseColor1
+ maximumLineCount: 1
+ visible: !d.communityGroupsExpanded
+ }
+
+ Item { Layout.fillWidth: true }
+
+ ManageTokenMenuButton {
+ currentIndex: visualIndex
+ count: d.controller.communityTokenGroupsModel.count
+ isGroup: true
+ groupId: model.communityId
+ onMoveRequested: (from, to) => d.controller.communityTokenGroupsModel.moveItem(from, to)
+ onShowHideGroupRequested: (groupId, flag) => d.controller.showHideGroup(groupId, flag)
+ }
+ }
+
+ StatusListView {
+ Layout.fillWidth: true
+ Layout.preferredHeight: contentHeight
+ model: d.controller.communityTokensModel
+ interactive: false
+ visible: d.communityGroupsExpanded
+
+ displaced: Transition {
+ NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
+ }
+
+ delegate: LocalTokenDelegate {
+ isGrouped: true
+ count: communityDelegateRoot.childCount
+ dragEnabled: count > 1
+ keys: ["x-status-draggable-community-token-item-%1".arg(model.communityId)]
+ bgColor: Theme.palette.indirectColor4
+ topInset: 2 // tighter "spacing"
+ bottomInset: 2
+ visible: communityDelegateRoot.communityId === model.communityId
+ }
+ }
+ }
+ }
+ }
+
+ contentItem: ColumnLayout {
+ spacing: Style.current.padding
+
+ StatusListView {
+ Layout.fillWidth: true
+ model: d.controller.regularTokensModel
+ implicitHeight: contentHeight
+ interactive: false
+
+ displaced: Transition {
+ NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
+ }
+
+ delegate: LocalTokenDelegate {
+ count: d.controller.regularTokensModel.count
+ dragEnabled: count > 1
+ keys: ["x-status-draggable-token-item"]
+ }
+ }
+
+ RowLayout {
+ id: communityTokensHeader
+ Layout.fillWidth: true
+ Layout.topMargin: Style.current.padding
+ visible: d.controller.communityTokensModel.count
+ StatusBaseText {
+ color: Theme.palette.baseColor1
+ text: qsTr("Community")// + " -> %1".arg(switchArrangeByCommunity.checked ? d.controller.communityTokenGroupsModel.count : d.controller.communityTokensModel.count)
+ }
+ Item { Layout.fillWidth: true }
+ StatusSwitch {
+ LayoutMirroring.enabled: true
+ LayoutMirroring.childrenInherit: true
+ id: switchArrangeByCommunity
+ textColor: Theme.palette.baseColor1
+ text: qsTr("Arrange by community")
+ }
+ }
+
+ StatusModalDivider {
+ Layout.fillWidth: true
+ Layout.topMargin: -Style.current.halfPadding
+ visible: communityTokensHeader.visible && switchArrangeByCommunity.checked
+ }
+
+ StatusLinkText {
+ Layout.alignment: Qt.AlignTrailing
+ visible: communityTokensHeader.visible && switchArrangeByCommunity.checked
+ text: d.communityGroupsExpanded ? qsTr("Collapse all") : qsTr("Expand all")
+ normalColor: linkColor
+ font.weight: Font.Normal
+ onClicked: d.communityGroupsExpanded = !d.communityGroupsExpanded
+ }
+
+ Loader {
+ Layout.fillWidth: true
+ active: d.controller.communityTokensModel.count
+ visible: active
+ sourceComponent: switchArrangeByCommunity.checked ? cmpCommunityTokenGroups : cmpCommunityTokens
+ }
+
+ StatusBaseText {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.current.padding
+ color: Theme.palette.baseColor1
+ text: qsTr("Hidden")// + " -> %1".arg(d.controller.hiddenTokensModel.count)
+ visible: d.controller.hiddenTokensModel.count
+ }
+
+ StatusListView {
+ Layout.fillWidth: true
+ model: d.controller.hiddenTokensModel
+ implicitHeight: contentHeight
+ interactive: false
+
+ displaced: Transition {
+ NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
+ }
+
+ delegate: LocalTokenDelegate {
+ dragEnabled: false
+ keys: ["x-status-draggable-none"]
+ isHidden: true
+ }
+ }
+ }
+
+ Component {
+ id: cmpCommunityTokens
+ StatusListView {
+ model: d.controller.communityTokensModel
+ implicitHeight: contentHeight
+ interactive: false
+
+ displaced: Transition {
+ NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
+ }
+
+ delegate: LocalTokenDelegate {
+ count: d.controller.communityTokensModel.count
+ dragEnabled: count > 1
+ keys: ["x-status-draggable-community-token-item"]
+ }
+ }
+ }
+
+ Component {
+ id: cmpCommunityTokenGroups
+ StatusListView {
+ model: d.controller.communityTokenGroupsModel
+ implicitHeight: contentHeight
+ interactive: false
+ spacing: Style.current.halfPadding
+
+ displaced: Transition {
+ NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
+ }
+
+ delegate: LocalTokenGroupDelegate {}
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir
index 4f006f8f963..003160c8278 100644
--- a/ui/app/AppLayouts/Wallet/panels/qmldir
+++ b/ui/app/AppLayouts/Wallet/panels/qmldir
@@ -2,3 +2,4 @@ WalletHeader 1.0 WalletHeader.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml
WalletNftPreview 1.0 WalletNftPreview.qml
ActivityFilterPanel 1.0 ActivityFilterPanel.qml
+ManageTokensPanel 1.0 ManageTokensPanel.qml
diff --git a/ui/imports/shared/controls/InformationTag.qml b/ui/imports/shared/controls/InformationTag.qml
index 467d45ba567..cfa2816244c 100644
--- a/ui/imports/shared/controls/InformationTag.qml
+++ b/ui/imports/shared/controls/InformationTag.qml
@@ -11,8 +11,8 @@ import utils 1.0
Control {
id: root
- property alias image : image
- property alias iconAsset : iconAsset
+ property alias image: image
+ property alias iconAsset: iconAsset
property alias tagPrimaryLabel: tagPrimaryLabel
property alias tagSecondaryLabel: tagSecondaryLabel
property alias middleLabel: middleLabel
@@ -31,63 +31,54 @@ Control {
QtObject {
id: d
- property var loadingComponent: Component { LoadingComponent {}}
+ property var loadingComponent: Component { LoadingComponent {} }
}
- horizontalPadding: Style.current.halfPadding
- verticalPadding: 5
+ horizontalPadding: 12
+ verticalPadding: 8
+ spacing: 4
background: Loader {
sourceComponent: root.loading ? d.loadingComponent : root.customBackground
}
contentItem: RowLayout {
- spacing: 4
+ spacing: root.spacing
visible: !root.loading
// FIXME this could be StatusIcon but it can't load images from an arbitrary URL
Image {
id: image
- Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: visible ? 16 : 0
Layout.maximumHeight: visible ? 16 : 0
- visible: image.source !== ""
+ visible: !!source
}
StatusIcon {
id: iconAsset
- Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: visible ? 16 : 0
Layout.maximumHeight: visible ? 16 : 0
- visible: iconAsset.icon !== ""
+ visible: !!icon
}
StatusBaseText {
id: tagPrimaryLabel
- Layout.alignment: Qt.AlignVCenter
font.pixelSize: Style.current.tertiaryTextFontSize
- font.weight: Font.Normal
- color: Theme.palette.directColor1
visible: text !== ""
}
StatusBaseText {
id: middleLabel
- Layout.alignment: Qt.AlignVCenter
font.pixelSize: Style.current.tertiaryTextFontSize
- font.weight: Font.Normal
color: Theme.palette.baseColor1
visible: text !== ""
}
StatusBaseText {
id: tagSecondaryLabel
- Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: root.secondarylabelMaxWidth
font.pixelSize: Style.current.tertiaryTextFontSize
- font.weight: Font.Normal
color: Theme.palette.baseColor1
visible: text !== ""
elide: Text.ElideMiddle
}
Loader {
id: rightComponent
- Layout.alignment: Qt.AlignVCenter
}
}
}