/* * This file is part of the Flowee project * Copyright (C) 2025-2026 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 "../Flowee" as Flowee import "../Utils.js" as Utils import Flowee.org.pay Page { id: root headerText: qsTr("Scheduled Payment") // This is an instance of the C++ SavedPayment class (SavedPaymentsHandler.h) required property QtObject savedPayment property Payment payment: savedPayment.payment property bool withScheduleButton: false property var acceptHandler: function handler() { thePile.pop() } Flickable { id: scrollPane anchors.fill: parent contentWidth: width contentHeight: column.height Column { id: column width: parent.width PageTitledBox { title: qsTr("Comment") width: parent.width EditableLabel { text: root.payment.userComment width: parent.width onEdited: root.payment.userComment = text } } PageTitledBox { title: qsTr("Payment Amount") id: amountsBox width: parent.width property var detail: { let details = root.payment.details for (let index in details) { let detail = details[index] if (detail.isOutput) return detail } return null } Row { spacing: 10 Rectangle { property bool main: amountsBox.detail.fiatFollows width: Math.max(priceBch.implicitWidth, 140) height: 80 color: "#00000000" radius: 6 border.width: 1.3 border.color: main ? palette.highlight : color opacity: main ? 1 : 0.7 Image { id: pinButton y: -5 width: 28 height: 40 anchors.horizontalCenter: parent.horizontalCenter visible: parent.main source: "qrc:/pin-green.svg" } MouseArea { anchors.fill: parent onClicked: priceBch.forceActiveFocus() } Flowee.BitcoinValueField { id: priceBch value: amountsBox.detail.paymentAmount backgroundEnabled: false anchors.horizontalCenter: parent.horizontalCenter anchors.top: pinButton.bottom anchors.topMargin: 5 focus: false onValueEdited: amountsBox.detail.paymentAmount = value onActiveFocusChanged: { if (activeFocus) { amountsBox.detail.fiatFollows = true numKeyboardWidget.start(money) } } activeFocusOnTab: false } } Rectangle { property bool main: !amountsBox.detail.fiatFollows width: Math.max(priceFiat.implicitWidth, 140) height: 80 color: "#00000000" radius: 6 border.width: 1.3 opacity: main ? 1 : 0.7 border.color: main ? palette.highlight : color Image { id: pinButton2 y: -5 width: 28 height: 40 anchors.horizontalCenter: parent.horizontalCenter visible: parent.main source: "qrc:/pin-green.svg" transform: Scale { xScale: -1; yScale: 1; origin.x: 15 } } MouseArea { anchors.fill: parent onClicked: priceFiat.forceActiveFocus() } Flowee.FiatValueField { id: priceFiat value: amountsBox.detail.paymentAmountFiat backgroundEnabled: false anchors.horizontalCenter: parent.horizontalCenter anchors.top: pinButton2.bottom anchors.topMargin: 5 focus: false activeFocusOnTab: false onValueEdited: amountsBox.detail.paymentAmountFiat = value visible: Pay.isMainChain onActiveFocusChanged: { if (activeFocus) { amountsBox.detail.fiatFollows = false numKeyboardWidget.start(money) } } } } } NumericKeyboardWidget { id: numKeyboardWidget width: parent.width dataInput: ourData opacity: 0 visible: opacity > 0 function start(money) { ourData.editor = money if (!numKeyboardWidget.visible) { var pos = valueComment.mapToItem(scrollPane.contentItem, 0, implicitHeight) // bottom of keyboard numKeyboardWidget.opacity = 1 // will make the item visible scrollPane.contentY = 10 + pos.y - scrollPane.height } } Item { id: ourData property QtObject editor: null// Item {} function shake() {} } Behavior on opacity { NumberAnimation { } } } Flowee.BigButton { visible: numKeyboardWidget.visible text: qsTr("Close") anchors.horizontalCenter: numKeyboardWidget.horizontalCenter onClicked: { valueComment.forceActiveFocus() numKeyboardWidget.opacity = 0 } } Flowee.Label { id: valueComment width: parent.width text: qsTr("Pinned amount will be protected from exchange rate fluctuations.") font.italic: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere focus: true } } PageTitledBox { id: planningBox title: qsTr("Planning") width: parent.width CalendarTextButton { id: nextCalendar width: parent.width text: qsTr("Next Payment") + ":" date: root.savedPayment.next onSelectedDateChanged: root.savedPayment.updateStartDate(selectedDate) Flowee.Label { text: qsTr("(date is in the past)") height: 50 font.bold: true verticalAlignment: Text.AlignVCenter visible: { let target = root.savedPayment.next let now = new Date() return target < now } } } Repeater { model: root.savedPayment.configs Item { width: planningBox.width height: planningPane.height Column { id: planningPane width: parent.width Flowee.RadioButton { id: aRadioButton focus: true text: qsTr("By weekday") checked: modelData.dayOfWeek !== -1 visible: index === 0 // don't repeat this for each config width: parent.width onClicked: root.savedPayment.switchToWeek() } Flowee.RadioButton { text: qsTr("By day of month") focus: true checked: modelData.dayOfMonth !== -1 visible: index === 0// don't repeat this for each config width: parent.width onClicked: root.savedPayment.switchToMonth() } Flowee.Label { id: startTimeLabel x: aRadioButton.textAlignOffset height: Math.max(50, contentHeight + 10) verticalAlignment: Text.AlignVCenter text: { let loc = Qt.locale() var day = modelData.dayOfWeek if (day >= 0) return loc.standaloneDayName(day, Locale.LongFormat) day = modelData.dayOfMonth if (day > 0) return day return "" } MouseArea { width: root.width / 2 x: -5 y: -5 height: parent.height + 10 onClicked: picker.startDayPicker() } } Flow { id: repeatIntervalFlow spacing: 6 width: parent.width - x Flowee.Label { id: repeatLabel text: qsTr("Repeat Interval") + ":" font.bold: true height: Math.max(50, contentHeight + 10) verticalAlignment: Text.AlignVCenter MouseArea { width: root.width - 10 height: parent.height x: -5 onClicked: picker.startRepeatPicker() } } Flowee.Label { height: repeatLabel.height verticalAlignment: Text.AlignVCenter text: { var week = modelData.dayOfWeek if (week >= 0) { let interval = modelData.weekInterval if (interval <= 1) return qsTr("Every week", "repetition") return qsTr("Once every %1 weeks", "repetition", interval).arg(interval) } var month = modelData.dayOfMonth if (month >= 0) { let interval = modelData.monthInterval if (interval <= 0) return qsTr("Every month", "repetition") return qsTr("Once every %1 months", "repetition", interval).arg(interval) } return "" } } } CalendarTextButton { width: parent.width visible: index === 0 // don't repeat this for each config text: qsTr("End Date") + ":" date: root.payment.repeat.sunset onSelectedDateChanged: root.payment.repeat.sunset = selectedDate } ListView { id: calendarsList width: parent.width height: 90 orientation: ListView.Horizontal model: 12 property QtObject config: modelData spacing: 10 delegate: Item { width: miniCalendar.width height: miniCalendar.height MiniCalendarWidget { id: miniCalendar month: { var date = new Date() // now date.setDate(1) date.setMonth(date.getMonth() + index) return date } highlights: { var hls = [] var source = calendarsList.config.predictedPayments var thisMonth = month for (let date of source) { if (date.getFullYear() === thisMonth.getFullYear() && date.getMonth() === thisMonth.getMonth()) hls.push(date.getDate()) } return hls } } } } } Flowee.CloseIcon { visible: index > 0 scale: 0.6 anchors.right: parent.right anchors.rightMargin: 20 anchors.top: parent.top anchors.bottomMargin: startTimeLabel.height + 8 onClicked: root.savedPayment.remove(modelData) } Rectangle { color: "#00000000" radius: 15 width: 30 height: 30 anchors.right: parent.right anchors.rightMargin: 20 anchors.top: parent.top anchors.bottomMargin: startTimeLabel.height + 8 visible: index === 0 border.width: 1 border.color: palette.text Rectangle { color: palette.text width: 1 height: parent.height * 0.5 anchors.centerIn: parent } Rectangle { color: palette.text height: 1 width: parent.width * 0.5 anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: root.savedPayment.add() } } Flowee.Dialog { id: picker property bool isDayPicker: true function startDayPicker() { isDayPicker = true open() } function startRepeatPicker() { isDayPicker = false open() } property QtObject config: modelData standardButtons: QQC2.DialogButtonBox.Cancel contentComponent: Flickable { width: picker.width - 40 height: Math.min(400, chooserColumn.height) contentWidth: width contentHeight: chooserColumn.height clip: true Column { id: chooserColumn width: parent.width Repeater { model: picker.isDayPicker ? (modelData.dayOfWeek >= 0 ? 7 : 31) : 12 Item { width: picker.width - 40 height: chooserLabel.height + 12 Flowee.Label { id: chooserLabel anchors.centerIn: parent text: { if (!picker.isDayPicker || picker.config.dayOfMonth >= 0) return "" + (modelData + 1) return Qt.locale().standaloneDayName(modelData, Locale.LongFormat) } } MouseArea { anchors.fill: parent onClicked: { if (picker.isDayPicker) { if (picker.config.dayOfMonth >= 0) picker.config.dayOfMonth = modelData + 1 else picker.config.dayOfWeek = modelData } else { // repeat interval picker if (picker.config.dayOfMonth >= 0) picker.config.monthInterval = modelData + 1 else picker.config.weekInterval = modelData + 1 } picker.close() } } } } } } } } } } PageTitledBox { title: qsTr("Wallet for Payment") id: walletSelector width: parent.width visible: !portfolio.singleAccountSetup AccountSelectorWidget { onSelectedAccountChanged: payment.account = selectedAccount startingAccount: root.payment.account } } Item { width: 1; height: 20; visible: scheduleButton.visible } // spacer Flowee.BigButton { id: scheduleButton width: parent.width * 0.75 anchors.horizontalCenter: parent.horizontalCenter isMainButton: true text: qsTr("Schedule") visible: root.withScheduleButton onClicked: { Pay.addRepeatPayment(root.payment) root.acceptHandler() } } Item { width: 1; height: 20 } // spacer PageTitledBox { title: qsTr("Payment Details") // Flowee.Label { // text: "Fee per byte: " + root.payment.feePerByte // } Repeater { model: root.payment.details Loader { width: root.width sourceComponent: { if (model.isOutput) return outputDetails if (model.isComment) return opReturnDetails return undefined } onLoaded: item.outputDetail = model } } } } } Keys.onPressed: (event)=> { if ((event.key === Qt.Key_Escape || event.key === Qt.Key_Back) && numKeyboardWidget.visible) { valueComment.forceActiveFocus() // move focus away from the input widgets to avoid blinking cursor numKeyboardWidget.opacity = 0 event.accepted = true } } Item { // data Component { id: outputDetails Column { id: delegateRoot spacing: 6 property QtObject outputDetail: null Flowee.Label { text: qsTr("Destination") } Flowee.Label { text: delegateRoot.outputDetail.niceAddress x: 10 font.italic: true } Flowee.BitcoinAmountLabel { id: amountBch value: delegateRoot.outputDetail.paymentAmount x: 10 colorize: false visible: delegateRoot.outputDetail.fiatFollows } Flowee.Label { id: amountFiat text: Fiat.formattedPrice(delegateRoot.outputDetail.paymentAmountFiat) x: 10 visible: !delegateRoot.outputDetail.fiatFollows } } } Component { id: opReturnDetails Column { property QtObject outputDetail: null id: delegateRoot spacing: 6 Flowee.Label { text: qsTr("Public Comment") } Flowee.Label { x: 10 font.italic: true text: delegateRoot.outputDetail.preview } } } } }