Files
pay/guis/mobile/RepeatPaymentDetails.qml
2026-03-14 21:14:42 +01:00

570 lines
25 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2025-2026 Tom Zander <tom@flowee.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}