/* * This file is part of the Flowee project * Copyright (C) 2022-2025 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick import QtQuick.Controls.Basic as QQC2 import QtQuick.Layouts import QtQuick.Effects import "../Flowee" as Flowee QQC2.Control { id: root width: parent.width height: parent.height background: Rectangle { color: palette.light } // This trick means any child items are actually added as the 'stack' item's children. default property alias content: stack.children property int currentIndex: 0 property bool showFilterIcon: false /// the gradient at the bottom of the header property bool showHeaderGradient: true signal filterIconClicked; onCurrentIndexChanged: stack.newCurrentItem() Component.onCompleted: stack.newCurrentItem() function switchToTab(index) { currentIndex = index } // called from main when this page becomes active, as well as when we change tabs function takeFocus() { // try to make sure the current tab gets keyboard focus properly. // We do some extra work in case the tab itself is a loader. forceActiveFocus(); let visibleChild = stack.children[currentIndex]; visibleChild.focus = true if (visibleChild instanceof Loader) { var child = visibleChild.item; if (child !== null) child.focus = true; } } Rectangle { id: offlineNotice color: mainWindow.errorRedBg width: parent.width height: visible ? 35 : 0 visible: Pay.deviceOffline Flowee.Label { text: qsTr("No Internet Available"); font.bold: true anchors.centerIn: parent } Behavior on height { NumberAnimation { } } } Rectangle { id: header width: parent.width y: offlineNotice.height height: 50 + Pay.screenInsets.y color: Pay.useDarkSkin ? palette.window : mainWindow.floweeBlue Flowee.HamburgerMenu { id: menuButton y: 14 + Pay.screenInsets.y color: "white" wide: true x: 20 + Pay.screenInsets.x } MouseArea { anchors.fill: menuButton // make the touch area a lot bigger, its safe as its limited to the 'header' area anyway. anchors.margins: -60 cursorShape: Qt.PointingHandCursor onClicked: menuOverlay.open = true; } QQC2.Control { id: logo // Here we just want the text part. So clip that out. clip: true anchors.top: menuButton.top anchors.left: menuButton.right anchors.leftMargin: 3 // x: 36 width: 122 height: 21 baselineOffset: 16 Image { source: "qrc:/FloweePay.svg" // ratio: 449 / 77 width: 150 height: 26 x: -28 y: -5 } } QQC2.Label { id: currentWalletName visible: !portfolio.singleAccountSetup text: portfolio.current.name color: "#fcfcfc" clip: true anchors.left: logo.right anchors.right: filterIcon.left anchors.topMargin: -2 anchors.leftMargin: 10 anchors.baseline: logo.baseline MouseArea { anchors.fill: parent onClicked: { if (accountSelector.visible) accountSelector.close(); else accountSelector.open(); } } } Image { id: filterIcon source: "qrc:/filter-light.svg" opacity: root.showFilterIcon ? 1 : 0 visible: opacity > 0.1 anchors.right: parent.right anchors.rightMargin: 16 anchors.bottom: parent.bottom anchors.bottomMargin: 14 smooth: true width: 25 height: 25 MouseArea { anchors.fill: parent anchors.margins: -10 onClicked: root.filterIconClicked(); } // and a little counter for the number of filters active // to make it easier to see if stuff may be hidden. // Notice that the MainViewBase has one instance for the // entire application, while the filters are per acccount. // So we need a bit of extra work to notice changes as the // two 'Connections' objects below show. property int numFilters: calcNumFilters(); function calcNumFilters() { var filterCount = 0; var currentAccount = portfolio.current if (currentAccount !== null) filterCount = currentAccount.transactions.filterCount; return filterCount; } Connections { target: portfolio function onCurrentChanged() { filterChangedConnection.target = portfolio.current.transactions filterIcon.numFilters = filterIcon.calcNumFilters(); } } Connections { id: filterChangedConnection target: portfolio.current.transactions function onIncludeFlagsChanged() { filterIcon.numFilters = filterIcon.calcNumFilters(); } } Rectangle { color: "#1d6828" width: 18 height: 18 radius: 9 anchors.bottom: parent.bottom anchors.bottomMargin: -8 x: -4 visible: filterIcon.numFilters > 0 Text { anchors.centerIn: parent text: filterIcon.numFilters color: "#fcfcfc" } } Behavior on opacity { NumberAnimation { easing.type: Easing.OutExpo duration: 250 } } } AccountSelectorPopup { id: accountSelector x: Pay.screenInsets.x width: root.width - x - Pay.screenInsets.width y: header.height onSelectedAccountChanged: portfolio.current = selectedAccount } property var headerGradient: Gradient { GradientStop { position: 0.9; color: header.color } GradientStop { position: 1; color: { let c = header.color; return Qt.rgba(c.r, c.g, c.b, 0); } } } gradient: (!Pay.useDarkSkin || root.showHeaderGradient) ? headerGradient : undefined } Item { id: stack x: Pay.screenInsets.x width: root.width - Pay.screenInsets.width anchors.top: header.bottom; anchors.bottom: tabbar.top function newCurrentItem() { var current = root.currentIndex; for (let i = 0; i < stack.children.length; ++i) { let on = i === current let child = stack.children[i]; child.visible = on; } root.takeFocus(); var page = stack.children[current]; var showGradient = true; if (page !== null && typeof(page.showHeaderGradient) == "boolean") { showGradient = page.showHeaderGradient; } root.showHeaderGradient = showGradient; } } Rectangle { anchors.fill: tabbar color: palette.window anchors.bottomMargin: -Pay.screenInsets.height } Row { id: tabbar anchors.bottom: parent.bottom anchors.bottomMargin: Pay.screenInsets.height Repeater { model: stack.children.length delegate: Item { height: 65 width: root.width / stack.children.length; Rectangle { x: 5 height: 4 width: parent.width - 10 color: palette.highlight visible: modelData === root.currentIndex } Rectangle { anchors.fill: parent color: palette.highlight visible: modelData === root.currentIndex opacity: 0.15 } Image { source: stack.children[modelData].icon width: 30 height: 30 smooth: true y: 8 anchors.horizontalCenter: parent.horizontalCenter } Flowee.Label { id: buttonLabel text: stack.children[modelData].title anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 2 font.pixelSize: 14 } NewIndicator { id: newIndicator buddy: buttonLabel Component.onCompleted: { var buttonId = stack.children[modelData].buttonId; if (typeof(buttonId) === "number") newIndicator.buttonId = buttonId; } } MouseArea { anchors.fill: parent onClicked: { newIndicator.markSeen(); root.currentIndex = modelData } } } } } Item { id: floatingCard width: mainText.width + 32 height: 28 + mainText.height + 10 + bgSyncButton.height + 10 + hideButtonLabel.height + 16 y: 330 x: 10 opacity: Pay.showBGSyncCard ? 1 : 0 visible: opacity > 0.1 onXChanged: if (x < -160 || x < 40 - width) visible = false; Rectangle { radius: 5 width: parent.width - 20 height: parent.height - 20 anchors.centerIn: parent color: palette.window } Rectangle { color: palette.highlight opacity: 0.15 anchors.fill: parent radius: 8 // soft shadow layer.enabled: true layer.effect: MultiEffect { blurEnabled: true blur: 1 } } MouseArea { anchors.fill: parent acceptedButtons: Qt.AllButtons hoverEnabled: true // eat all mouse activity onClicked: (mouseEvent)=> { floatingCard.opacity = 0; if (mouseEvent.y > height - hideButtonLabel.height - 26) { // the hide button was hit. return; } thePile.push("./BackgroundSyncConfig.qml", { "autoEnableAll": "true" } ); } } DragHandler { id: dragHandler yAxis.minimum: header.height yAxis.maximum: root.height - parent.height xAxis.minimum: -200 xAxis.maximum: root.width - parent.width } Flowee.Label { id: mainText text: qsTr("For a better experience, enable Flowee Pay background synchronization.") x: 16 y: 28 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere width: 220 } Rectangle { id: bgSyncButton width: Math.max(160, bgSyncButtonLabel.width + 30) height: bgSyncButtonLabel.height + 20 radius: 10 color: mainWindow.floweeGreen anchors.top: mainText.bottom anchors.topMargin: 10 anchors.horizontalCenter: parent.horizontalCenter Flowee.Label { id: bgSyncButtonLabel text: qsTr("Start") color: "black" anchors.centerIn: parent } } Flowee.Label { id: hideButtonLabel text: qsTr("hide") opacity: 0.6 anchors.top: bgSyncButton.bottom anchors.topMargin: 10 anchors.horizontalCenter: parent.horizontalCenter } Behavior on opacity { NumberAnimation { } } } /* * Fully encrypted wallet are useless to inspect or use until they are opened. * This is an overlay that covers the entire screen except for the header to make * unlocking a "modal" dialogue. */ Rectangle { color: palette.window anchors.top: header.bottom anchors.bottom: parent.bottom width: parent.width visible: portfolio.current.needsPinToOpen && !portfolio.current.isDecrypted // eat all taps / clicks. MouseArea { anchors.fill: parent } // to avoid using resources in the 99% of the time the user is not unlocking his wallet, // we use a loader for the unlocking screen. Loader { id: unlockWalletWidget anchors.fill: parent source: parent.visible ? "./UnlockWalletPanel.qml" : "" } } Keys.onPressed: (event)=> { if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) { // when we are on another tab, we can go to 'main' on back. if (root.currentIndex !== 0) { root.currentIndex = 0; event.accepted = true; } } // in all other cases, propagate the event up the stack. } }